• Home
  • MSE papers
    • Guide to the papers
    • SC14 questions and tasks
    • Full MSE report
    • Technical paper
    • Response to review
  • Candidate MPs
    • CMP registry
    • MP43 focal hockeystick
    • MP44 focal power ramp
    • MP29 sensitivity
    • MP32 sensitivity
    • Earlier tuning controls
    • Parts of a CMP
    • How to specify a CMP
  • Annual use
    • Annual calculation
    • Input data
    • Worked example
  • Results & background
    • Results explorer
    • Scorecard
    • Scorecard: best-relative weights
    • SCW17 background and results
    • How MSE fits
    • Sources & changes
    • Agreement checks
  1. Results and background
  2. Interactive CMP scorecard: best-relative weighting
  • SPRFMO Jack Mackerel Candidate Management Procedures
  • MSE papers
    • Guide to the Jack Mackerel MSE papers for SC14
    • Jack mackerel MSE: questions and tasks for SC14
    • Jack mackerel management strategy evaluation for SC14
    • Jack mackerel MSE: technical information and work remaining
    • Response to the independent SC14 MSE desk review
  • Candidate management procedures
    • CMP registry
    • MP43 focal hockeystick case
    • MP44 focal power-ramp case
    • MP29 hockeystick candidate
    • MP32 powerramp candidate
    • Earlier trial and robustness controls
    • Management-procedure components
    • CMP specification template
  • Annual use
    • Annual CMP application
    • Illustrative 2027 TAC calculation
    • Annual input data
  • Results and background
    • Load candidates into Slick
    • Interactive CMP scorecard explorer
    • Interactive CMP scorecard: best-relative weighting
    • CMP results and development history
    • How the MSE work fits together
    • Sources and record of changes
    • Checks of agreement among the SC14 files
  1. Results and background
  2. Interactive CMP scorecard: best-relative weighting

Interactive CMP scorecard: best-relative weighting

Alternative browser application using the current reference-OM performance metrics

This browser-based explorer reproduces the relative-preference scorecard without requiring R or a Shiny server. Select CMPs and metrics, choose a weighting scheme, or enter weights manually. Scores are recalculated relative to the selected CMP set and are not absolute acceptability scores or agreed management preferences.

NoteBest-relative weighting version

This alternative version weights each CMP relative to the best performer within each performance indicator (PI) and selects that method by default.1 The weights applied across PIs are still selected separately. The original range-weight scorecard remains available for comparison.

Within-PI CMP weighting

This method gives the best CMP a weight of 100 for each PI and weights other CMPs proportionally.

Weights across PIs

CMPs

Performance metrics

CMP relative trade-off scores

Selected weights across PIs

Performance quilt

Each cell shows the raw PI value, followed on the next line by its parenthesized within-PI CMP weight (W); for example, (98.2) means W = 98.2. Purple indicates lower and light lavender higher relative performance.

Footnotes

  1. For each PI \(x\), the best-performing CMP among the \(N\) selected CMPs is assigned a weight of 100. For a PI where higher values are preferred, CMP \(i\) receives \(W_{i,x}=100\,V_{i,x}/V_{\mathrm{best},x}\), where \(V_{\mathrm{best},x}=\max_j(V_{j,x})\). For a PI where lower values are preferred, the direction-consistent reciprocal is used: \(W_{i,x}=100\,V_{\mathrm{best},x}/V_{i,x}\), where \(V_{\mathrm{best},x}=\min_j(V_{j,x})\). Thus the best CMP has \(W_{i,x}=100\), while the other weights retain their proportional contrast with the best observed value. The weights are recalculated whenever the selected CMP set changes; they express relative performance, not an absolute acceptability threshold.↩︎

Interactive CMP scorecard explorer
CMP results and development history
Source Code
---
title: "Interactive CMP scorecard: best-relative weighting"
subtitle: "Alternative browser application using the current reference-OM performance metrics"
toc: false
page-layout: full
---

This browser-based explorer reproduces the relative-preference scorecard
without requiring R or a Shiny server. Select CMPs and metrics, choose a
weighting scheme, or enter weights manually. Scores are recalculated relative
to the selected CMP set and are **not absolute acceptability scores or agreed
management preferences**.

::: {.callout-note title="Best-relative weighting version"}
This alternative version **weights each CMP relative to the best performer**
within each performance indicator (PI) and selects that method by
default.[^best-relative-weighting] The weights applied across PIs are still
selected separately. The [original range-weight scorecard](scorecard.qmd)
remains available for comparison.
:::

[^best-relative-weighting]: For each PI $x$, the best-performing CMP among the $N$ selected CMPs is assigned a weight of 100. For a PI where higher values are preferred, CMP $i$ receives $W_{i,x}=100\,V_{i,x}/V_{\mathrm{best},x}$, where $V_{\mathrm{best},x}=\max_j(V_{j,x})$. For a PI where lower values are preferred, the direction-consistent reciprocal is used: $W_{i,x}=100\,V_{\mathrm{best},x}/V_{i,x}$, where $V_{\mathrm{best},x}=\min_j(V_{j,x})$. Thus the best CMP has $W_{i,x}=100$, while the other weights retain their proportional contrast with the best observed value. The weights are recalculated whenever the selected CMP set changes; they express relative performance, not an absolute acceptability threshold.

<style>
.scorecard-app { --panel-border:#d9dde3; --muted:#5d6670; }
.scorecard-controls { display:grid; grid-template-columns:repeat(2,minmax(280px,1fr)); gap:1rem; margin:1rem 0; }
.scorecard-panel { border:1px solid var(--panel-border); border-radius:.45rem; padding:1rem; background:#fff; }
.scorecard-panel h3 { margin-top:0; font-size:1.05rem; }
.scorecard-options { max-height:260px; overflow:auto; display:grid; gap:.3rem; }
.scorecard-options label { display:flex; gap:.45rem; align-items:center; font-weight:400; }
.scorecard-custom { display:none; margin-top:.8rem; }
.scorecard-custom.active { display:grid; gap:.35rem; }
.scorecard-custom label { display:grid; grid-template-columns:1fr 90px; gap:.5rem; align-items:center; }
.scorecard-custom input { width:100%; }
.scorecard-output { margin-top:1rem; }
#score-plot { width:100%; min-height:430px; }
.scorecard-table-wrap { overflow-x:auto; }
.scorecard-app table { width:100%; border-collapse:collapse; margin-top:.75rem; }
.scorecard-app th,.scorecard-app td { border-bottom:1px solid #e3e6ea; padding:.45rem .55rem; text-align:right; }
.scorecard-app th:first-child,.scorecard-app td:first-child { text-align:left; }
.quilt-cell { text-align:center!important; min-width:95px; color:#18141d; }
.quilt-weight { display:block; margin-top:.15rem; font-size:.78rem; font-weight:400; }
.scorecard-note { color:var(--muted); font-size:.92rem; }
.scorecard-error { color:#a12622; font-weight:600; min-height:1.5rem; }
@media (max-width:900px) { .scorecard-controls { grid-template-columns:1fr; } }
</style>

```{=html}
<div class="scorecard-app">
  <div class="scorecard-controls">
    <section class="scorecard-panel">
      <h3>Within-PI CMP weighting</h3>
      <select id="score-scaling" class="form-select">
        <option value="range">Range weights (original method)</option>
        <option value="best-relative" selected>Best-relative CMP weights</option>
      </select>
      <p class="scorecard-note">This method gives the best CMP a weight of 100 for each PI and weights other CMPs proportionally.</p>
    </section>
    <section class="scorecard-panel">
      <h3>Weights across PIs</h3>
      <select id="weight-scheme" class="form-select">
        <option value="equal">Equal weights</option>
        <option value="balanced">Balanced fishing and stock condition</option>
        <option value="dispersion" selected>Dispersion weighted (square-root CV)</option>
        <option value="custom">Specify weights manually</option>
      </select>
      <div id="custom-weights" class="scorecard-custom"></div>
    </section>
    <section class="scorecard-panel">
      <h3>CMPs</h3>
      <div id="cmp-options" class="scorecard-options"></div>
    </section>
    <section class="scorecard-panel">
      <h3>Performance metrics</h3>
      <div id="metric-options" class="scorecard-options"></div>
    </section>
  </div>

  <div id="scorecard-error" class="scorecard-error"></div>

  <section class="scorecard-panel scorecard-output">
    <h3>CMP relative trade-off scores</h3>
    <p id="scaling-note" class="scorecard-note"></p>
    <svg id="score-plot" role="img" aria-label="CMP score bar chart"></svg>
    <div id="score-table" class="scorecard-table-wrap"></div>
  </section>

  <section class="scorecard-panel scorecard-output">
    <h3>Selected weights across PIs</h3>
    <div id="weight-table" class="scorecard-table-wrap"></div>
  </section>

  <section class="scorecard-panel scorecard-output">
    <h3>Performance quilt</h3>
    <p class="scorecard-note">Each cell shows the raw PI value, followed on the next line by its parenthesized within-PI CMP weight (<em>W</em>); for example, (98.2) means <em>W</em> = 98.2. Purple indicates lower and light lavender higher relative performance.</p>
    <div id="quilt-table" class="scorecard-table-wrap"></div>
  </section>
</div>
```

<script>
(async function () {
  const dataUrl = "../data/candidates/candidate_scorecard_input_reference.json";
  const colors = {
    "HS+20 (MP29)":"#C87A8A", "HS-20 (MP43)":"#B6875B",
    "HSsym (MP45)":"#909646", "HS-30 (MP47)":"#55A067",
    "PR+20 (MP32)":"#00A396", "PR-20 (MP44)":"#409BBB",
    "PRsym (MP46)":"#9189C7", "PR-30 (MP48)":"#BE7AB4"
  };
  const stockMetrics = new Set([
    "SB / SB[MSY]", "F / F[MSY]", "VB / VB[2025]", "VB / VB[MSY]",
    "P(Kobe red)"
  ]);
  const fishingMetrics = new Set(["Catch", "IACC", "Mean catch reduction"]);
  const unique = xs => [...new Set(xs)];
  const mean = xs => xs.reduce((a,b) => a+b, 0) / xs.length;
  const sd = xs => {
    if (xs.length < 2) return 0;
    const m = mean(xs);
    return Math.sqrt(xs.reduce((s,x) => s+(x-m)*(x-m), 0)/(xs.length-1));
  };
  const escapeHtml = value => String(value)
    .replaceAll("&","&amp;").replaceAll("<","&lt;")
    .replaceAll(">","&gt;").replaceAll('"',"&quot;");

  let rows;
  try {
    const response = await fetch(dataUrl);
    if (!response.ok) throw new Error(`Data request failed (${response.status})`);
    rows = await response.json();
  } catch (error) {
    document.getElementById("scorecard-error").textContent =
      `Could not load scorecard data: ${error.message}`;
    return;
  }

  const cmps = unique(rows.map(d => d.mp));
  const metrics = unique(rows.map(d => d.metric));
  const defaultMetrics = unique(rows.filter(d => d.include).map(d => d.metric));

  function optionList(containerId, values, selected, prefix) {
    document.getElementById(containerId).innerHTML = values.map((value, i) =>
      `<label><input type="checkbox" id="${prefix}-${i}" value="${escapeHtml(value)}" ${selected.includes(value)?"checked":""}> ${escapeHtml(value)}</label>`
    ).join("");
  }
  optionList("cmp-options", cmps, cmps, "cmp");
  optionList("metric-options", metrics, defaultMetrics, "metric");

  function selectedValues(containerId) {
    return [...document.querySelectorAll(`#${containerId} input:checked`)]
      .map(input => input.value);
  }

  function normalize(selectedRows, selectedMetrics, scaling) {
    const output = selectedRows.map(d => ({...d}));
    selectedMetrics.forEach(metric => {
      const subset = output.filter(d => d.metric === metric);
      const values = subset.map(d => Number(d.raw_value));
      const lo = Math.min(...values), hi = Math.max(...values), span = hi-lo;
      const directions = unique(subset.map(d => d.preferred_direction));
      if (directions.length !== 1)
        throw new Error(`PI has inconsistent preferred directions: ${metric}`);
      if (values.some(value => !Number.isFinite(value)))
        throw new Error(`PI contains a non-finite value: ${metric}`);
      subset.forEach(d => {
        const value = Number(d.raw_value);
        if (scaling === "best-relative") {
          if (lo < 0)
            throw new Error(`Best-relative scoring requires non-negative PI values: ${metric}`);
          if (d.preferred_direction === "higher is better")
            d.preference = hi === 0 ? 1 : value/hi;
          else if (d.preferred_direction === "lower is better")
            d.preference = lo === 0 ? (value === 0 ? 1 : 0) : lo/value;
          else throw new Error(`Unknown preferred direction for ${metric}`);
        } else {
          if (span === 0) d.preference = 1;
          else if (d.preferred_direction === "higher is better")
            d.preference = (value-lo)/span;
          else if (d.preferred_direction === "lower is better")
            d.preference = (hi-value)/span;
          else throw new Error(`Unknown preferred direction for ${metric}`);
        }
      });
    });
    return output;
  }

  function renderCustomWeights(selectedMetrics) {
    const container = document.getElementById("custom-weights");
    const existing = Object.fromEntries([...container.querySelectorAll("input")]
      .map(input => [input.dataset.metric, input.value]));
    container.innerHTML = selectedMetrics.map((metric, i) =>
      `<label>${escapeHtml(metric)}<input type="number" min="0" step="0.1" value="${existing[metric] ?? 1}" data-metric="${escapeHtml(metric)}" id="custom-${i}"></label>`
    ).join("");
    container.classList.toggle("active",
      document.getElementById("weight-scheme").value === "custom");
    container.querySelectorAll("input").forEach(input =>
      input.addEventListener("input", update));
  }

  function calculateWeights(normalizedRows, selectedMetrics, scheme) {
    let raw = {};
    if (scheme === "equal") selectedMetrics.forEach(m => raw[m] = 1);
    if (scheme === "balanced") {
      const stock = selectedMetrics.filter(m => stockMetrics.has(m));
      const fishing = selectedMetrics.filter(m => fishingMetrics.has(m));
      const other = selectedMetrics.filter(m => !stockMetrics.has(m) && !fishingMetrics.has(m));
      stock.forEach(m => raw[m] = 0.5/stock.length);
      fishing.forEach(m => raw[m] = 0.5/fishing.length);
      other.forEach(m => raw[m] = 1/other.length);
    }
    if (scheme === "dispersion") selectedMetrics.forEach(metric => {
      const values = normalizedRows.filter(d => d.metric === metric)
        .map(d => Number(d.raw_value));
      const cv = Math.abs(mean(values)) > 0 ? sd(values)/Math.abs(mean(values)) : 0;
      raw[metric] = Math.sqrt(Number.isFinite(cv) ? cv : 0);
    });
    if (scheme === "custom") selectedMetrics.forEach(metric => {
      const input = [...document.querySelectorAll("#custom-weights input")]
        .find(el => el.dataset.metric === metric);
      raw[metric] = Math.max(0, Number(input?.value ?? 0));
    });
    const total = Object.values(raw).reduce((a,b) => a+b, 0);
    if (!(total > 0)) throw new Error("At least one metric weight must be positive.");
    return Object.fromEntries(Object.entries(raw).map(([k,v]) => [k,v/total]));
  }

  function drawBars(scores) {
    const svg = document.getElementById("score-plot");
    const width=960, height=470, left=70, right=20, top=30, bottom=120;
    const innerW=width-left-right, innerH=height-top-bottom;
    const band=innerW/scores.length, barW=band*0.66;
    let markup = `<rect width="${width}" height="${height}" fill="white"/>`;
    [0,25,50,75,100].forEach(tick => {
      const y=top+innerH-(tick/100)*innerH;
      markup += `<line x1="${left}" x2="${width-right}" y1="${y}" y2="${y}" stroke="#dfe2e6"/><text x="${left-10}" y="${y+5}" text-anchor="end" font-size="13">${tick}</text>`;
    });
    scores.forEach((d,i) => {
      const x=left+i*band+(band-barW)/2, h=(d.score/100)*innerH, y=top+innerH-h;
      markup += `<rect x="${x}" y="${y}" width="${barW}" height="${h}" fill="${colors[d.mp]||"#777"}"/>`;
      markup += `<text x="${x+barW/2}" y="${y-7}" text-anchor="middle" font-size="14">${d.score.toFixed(1)}</text>`;
      markup += `<text transform="translate(${x+barW/2},${top+innerH+18}) rotate(38)" text-anchor="start" font-size="12">${escapeHtml(d.mp)}</text>`;
    });
    markup += `<text transform="translate(18,${top+innerH/2}) rotate(-90)" text-anchor="middle" font-size="14">Relative trade-off score (0–100)</text>`;
    markup += `<text x="${left+innerW/2}" y="${height-8}" text-anchor="middle" font-size="14">CMP (ordered from highest score)</text>`;
    svg.setAttribute("viewBox",`0 0 ${width} ${height}`);
    svg.innerHTML=markup;
  }

  function tableHtml(headers, bodyRows, classes="") {
    return `<table class="${classes}"><thead><tr>${headers.map(h=>`<th>${escapeHtml(h)}</th>`).join("")}</tr></thead><tbody>${bodyRows.map(row=>`<tr>${row.map((cell,i)=>`<td${i?"":""}>${cell}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
  }

  function quiltColor(preference) {
    const low=[121,82,168], high=[244,239,248];
    return `rgb(${low.map((v,i)=>Math.round(v+(high[i]-v)*preference)).join(",")})`;
  }

  function update() {
    const selectedCmps=selectedValues("cmp-options");
    const selectedMetrics=selectedValues("metric-options");
    const scaling=document.getElementById("score-scaling").value;
    const error=document.getElementById("scorecard-error"); error.textContent="";
    renderCustomWeights(selectedMetrics);
    if (selectedCmps.length<2) { error.textContent="Select at least two CMPs."; return; }
    if (!selectedMetrics.length) { error.textContent="Select at least one metric."; return; }
    try {
      const filtered=rows.filter(d=>selectedCmps.includes(d.mp)&&selectedMetrics.includes(d.metric));
      const normalized=normalize(filtered,selectedMetrics,scaling);
      const weights=calculateWeights(normalized,selectedMetrics,
        document.getElementById("weight-scheme").value);
      const scores=selectedCmps.map(mp=>({mp,score:normalized.filter(d=>d.mp===mp)
        .reduce((sum,d)=>sum+100*d.preference*weights[d.metric],0)}))
        .sort((a,b)=>b.score-a.score || a.mp.localeCompare(b.mp));
      drawBars(scores);
      document.getElementById("scaling-note").textContent = scaling === "best-relative"
        ? "Within each PI, CMP weights are proportional to the best selected CMP (best weight = 100)."
        : "Within each PI, CMP weights use direction-aware min–max scaling across the selected CMPs (worst = 0; best = 100).";
      document.getElementById("score-table").innerHTML=tableHtml(
        ["Rank","CMP","Score"], scores.map((d,i)=>[i+1,escapeHtml(d.mp),d.score.toFixed(2)]));
      document.getElementById("weight-table").innerHTML=tableHtml(
        ["Metric","Weight (%)"], selectedMetrics.map(m=>[escapeHtml(m),(100*weights[m]).toFixed(2)]));
      const quiltRows=scores.map(s=>s.mp);
      const quiltBody=quiltRows.map(mp=>[escapeHtml(mp),...selectedMetrics.map(metric=>{
        const d=normalized.find(x=>x.mp===mp&&x.metric===metric);
        let label=Number(d.raw_value).toFixed(2);
        if (d.statistic==="C") label=Number(d.raw_value).toFixed(0);
        if (["SB0red","PC270"].includes(d.statistic)) label=(100*Number(d.raw_value)).toFixed(1)+"%";
        return `<span>${label}</span><span class="quilt-weight">(${(100*d.preference).toFixed(1)})</span>`;
      })]);
      let quilt=tableHtml(["CMP",...selectedMetrics],quiltBody,"quilt");
      document.getElementById("quilt-table").innerHTML=quilt;
      const cells=document.querySelectorAll("#quilt-table tbody tr");
      cells.forEach((tr,rowIndex)=>selectedMetrics.forEach((metric,colIndex)=>{
        const d=normalized.find(x=>x.mp===quiltRows[rowIndex]&&x.metric===metric);
        const td=tr.children[colIndex+1]; td.classList.add("quilt-cell");
        td.style.backgroundColor=quiltColor(d.preference);
      }));
    } catch (e) { error.textContent=e.message; }
  }

  document.getElementById("weight-scheme").addEventListener("change",update);
  document.getElementById("score-scaling").addEventListener("change",update);
  document.querySelectorAll("#cmp-options input,#metric-options input")
    .forEach(input=>input.addEventListener("change",update));
  update();
})();
</script>

SPRFMO Jack Mackerel Working Group

 
  • View source
  • Report an issue

Specifications and results are version controlled.