<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-07-05T00:47:46+00:00</updated><id>/feed.xml</id><title type="html">Mario Krapp, PhD</title><subtitle>Climate &amp; Earth System Science | Problem Solving | Innovation | Strategy | Quality</subtitle><author><name>Mario Krapp (© 2013-2025)</name><email>mariokrapp@gmail.com</email></author><entry><title type="html">Mortgage calculator</title><link href="/2026/07/04/amortisation.html" rel="alternate" type="text/html" title="Mortgage calculator" /><published>2026-07-04T00:00:00+00:00</published><updated>2026-07-04T00:00:00+00:00</updated><id>/2026/07/04/amortisation</id><content type="html" xml:base="/2026/07/04/amortisation.html"><![CDATA[<h1 id="mortgage-calculator">Mortgage calculator</h1>

<p>I don’t like the mortgage calculators banks put on their website.
You type in a bunch of numbers and then you get one number back.</p>

<p>Life moves much faster than this.
Now you think it’s 30 years, then it’s 25, mmmh, and how about an interest rate of 5.5% p.a. instead of 4.8% p.a.
You don’t want to type that in each time, do you?</p>

<p>These mortgage calculators are also not showing all the information.
Rarely do you see straight away how much interest you have to pay.
For a 30-year loan that can be as much as the principal loan you want to borrow.</p>

<p>Worst of all, you wouldn’t learn how <a href="https://en.wikipedia.org/wiki/Amortization_(accounting)" target="_blank">amortization</a> works.
And that would be a shame.</p>

<script src="https://d3js.org/d3.v7.min.js"></script>

<style>

    .note {
      max-width: 1050px;
      font-size: 14px;
      color: #555;
      line-height: 1.45;
      margin-bottom: 18px;
    }

    .controls {
      display: grid;
      grid-template-columns: 180px 1fr 130px;
      gap: 8px 12px;
      max-width: 1050px;
      align-items: center;
      margin-bottom: 18px;
      padding: 14px;
      background: white;
      border: 1px solid #ddd;
      border-radius: 8px;
    }

    label {
      font-size: 14px;
      font-weight: 600;
    }

    input[type="range"] {
      width: 100%;
    }

    .value {
      font-variant-numeric: tabular-nums;
      font-size: 14px;
      text-align: right;
    }

    .result {
      max-width: 1050px;
      margin: 0 0 16px 0;
      padding: 12px 14px;
      background: #fff;
      border: 1px solid #ddd;
      border-radius: 8px;
      font-size: 15px;
    }

    svg {
      background: white;
      border: 1px solid #ddd;
      border-radius: 8px;
      max-width: 100%;
      height: auto;
    }

    .axis-title {
      font-size: 14px;
      font-weight: 700;
      fill: #222;
    }

    .small {
      font-size: 11px;
      fill: #666;
    }

    .contour {
      fill: none;
      stroke: #333;
      stroke-width: 1.2;
      stroke-opacity: 0.65;
    }

    .contour.major {
      stroke-width: 1.8;
      stroke-opacity: 0.9;
    }

    .contour-label {
      font-size: 11px;
      font-weight: 700;
      fill: #222;
      paint-order: stroke;
      stroke: white;
      stroke-width: 4px;
      stroke-linejoin: round;
    }

    .guide-line {
      stroke: #1f77b4;
      stroke-width: 1.4;
      stroke-dasharray: 4 4;
    }

    .selected-point {
      fill: #1f77b4;
      stroke: white;
      stroke-width: 2.5;
      cursor: grab;
    }

    .readout-label {
      font-size: 13px;
      font-weight: 700;
      fill: #1f77b4;
      paint-order: stroke;
      stroke: white;
      stroke-width: 4px;
      stroke-linejoin: round;
    }

    .drag-capture {
      fill: transparent;
      cursor: crosshair;
    }

    .repayment-line {
      fill: none;
      stroke: #1f77b4;
      stroke-width: 3;
      stroke-opacity: 0.85;
    }

    .axis path,
    .axis line {
      stroke: #333;
    }

    .axis text {
      font-size: 11px;
    }
  </style>

<div class="controls">
  <label for="principal">Loan amount</label>
  <input id="principal" type="range" min="50000" max="1500000" step="10000" value="650000" />
  <div id="principalValue" class="value"></div>

  <label for="rate">Annual interest rate</label>
  <input id="rate" type="range" min="0.5" max="12" step="0.1" value="6.5" />
  <div id="rateValue" class="value"></div>

  <label for="term">Loan term</label>
  <input id="term" type="range" min="5" max="35" step="1" value="25" />
  <div id="termValue" class="value"></div>
</div>

<div class="result" id="result"></div>

<h1 id="technical-details">Technical details</h1>

<p>This is for those who want to see what’s going on under the hood.</p>

<p>Monthly repayments are conditional on the length of the loan term (horizontal axis) and annual interest rate (vertical axis).
Click on any point in the graph and the sliders above for <em>Loan term</em> and <em>Annual interest rate</em> will be updated.</p>

<p>Have a go and see what happens when you move left and right or up and down. 
Pay attention to the <em>Total interest</em>.
For example, if you try to follow one of the contour lines, say $4,000, from lower left to upper right, that means you’re still paying the same amount each month.
But the the total paid depends on where you are on that $4,000 contour line.</p>

<p>Now, imagine you have a much higher loan and the loan term extends to infinity. This way you could repay a higher loan indefinitely with a reasonable monthly repayment.
That’s what we call <strong><em>rent</em></strong>. 🤩</p>

<svg id="viz" width="1100" height="760" viewBox="0 0 1100 760"></svg>

<script>
  // ------------------------------------------------------------
  // Amortisation formula
  // ------------------------------------------------------------
  function monthlyPayment(principal, annualRatePct, years) {
    const monthlyRate = annualRatePct / 100 / 12;
    const n = years * 12;

    if (monthlyRate === 0) {
      return principal / n;
    }

    return principal * monthlyRate * Math.pow(1 + monthlyRate, n) /
      (Math.pow(1 + monthlyRate, n) - 1);
  }

  function fmtMoney(x) {
    return "$" + d3.format(",.0f")(x);
  }

  function fmtMoney2(x) {
    return "$" + d3.format(",.2f")(x);
  }

  function clamp(x, lo, hi) {
    if (!Number.isFinite(x)) return lo;
    return Math.max(lo, Math.min(hi, x));
  }

  // ------------------------------------------------------------
  // SVG setup
  // ------------------------------------------------------------
  const svg = d3.select("#viz");

  const W = 1100;
  const H = 760;

  const plot = {
    x0: 115,
    y0: 75,
    width: 850,
    height: 570
  };

  plot.x1 = plot.x0 + plot.width;
  plot.y1 = plot.y0 + plot.height;

  const rateDomain = [0.5, 12];
  const termDomain = [5, 35];

  // Term is horizontal, interest is vertical.
  // Both are linearly spaced.
  const xTerm = d3.scaleLinear()
    .domain(termDomain)
    .range([plot.x0, plot.x1]);

  const yRate = d3.scaleLinear()
    .domain(rateDomain)
    .range([plot.y1, plot.y0]);

  const principalInput = d3.select("#principal");
  const rateInput = d3.select("#rate");
  const termInput = d3.select("#term");

  const principalValue = d3.select("#principalValue");
  const rateValue = d3.select("#rateValue");
  const termValue = d3.select("#termValue");
  const result = d3.select("#result");

  // ------------------------------------------------------------
  // Static axes
  // ------------------------------------------------------------
  svg.append("text")
    .attr("class", "axis-title")
    .attr("x", (plot.x0 + plot.x1) / 2)
    .attr("y", 35)
    .attr("text-anchor", "middle")
    .text("Equal monthly repayment contours");

  svg.append("text")
    .attr("class", "small")
    .attr("x", (plot.x0 + plot.x1) / 2)
    .attr("y", 55)
    .attr("text-anchor", "middle")
    .text("Click or drag in the plot, or use the sliders");

  const xAxis = d3.axisBottom(xTerm)
    .tickValues(d3.range(5, 36, 5))
    .tickFormat(d => d + " years");

  const yAxis = d3.axisLeft(yRate)
    .tickValues([0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
    .tickFormat(d => d + "%");

  svg.append("g")
    .attr("class", "axis")
    .attr("transform", `translate(0,${plot.y1})`)
    .call(xAxis);

  svg.append("g")
    .attr("class", "axis")
    .attr("transform", `translate(${plot.x0},0)`)
    .call(yAxis);

  svg.append("text")
    .attr("class", "axis-title")
    .attr("x", (plot.x0 + plot.x1) / 2)
    .attr("y", plot.y1 + 55)
    .attr("text-anchor", "middle")
    .text("Loan term");

  svg.append("text")
    .attr("class", "axis-title")
    .attr("x", -((plot.y0 + plot.y1) / 2))
    .attr("y", 38)
    .attr("transform", "rotate(-90)")
    .attr("text-anchor", "middle")
    .text("Annual interest rate");

  // Light grid
  svg.append("g")
    .attr("opacity", 0.15)
    .selectAll("line.vertical-grid")
    .data(d3.range(5, 36, 5))
    .enter()
    .append("line")
    .attr("x1", d => xTerm(d))
    .attr("x2", d => xTerm(d))
    .attr("y1", plot.y0)
    .attr("y2", plot.y1)
    .attr("stroke", "#000");

  svg.append("g")
    .attr("opacity", 0.15)
    .selectAll("line.horizontal-grid")
    .data(d3.range(1, 13, 1))
    .enter()
    .append("line")
    .attr("x1", plot.x0)
    .attr("x2", plot.x1)
    .attr("y1", d => yRate(d))
    .attr("y2", d => yRate(d))
    .attr("stroke", "#000");

  // ------------------------------------------------------------
  // Dynamic layers
  // ------------------------------------------------------------
  const contourLayer = svg.append("g");
  const labelLayer = svg.append("g");

  const selectedContourLayer = svg.append("g");

  const termGuide = svg.append("line")
    .attr("class", "guide-line");

  const rateGuide = svg.append("line")
    .attr("class", "guide-line");

  const selectedPoint = svg.append("circle")
    .attr("class", "selected-point")
    .attr("r", 8);

  const selectedLabel = svg.append("text")
    .attr("class", "readout-label");

  const dragCapture = svg.append("rect")
    .attr("class", "drag-capture")
    .attr("x", plot.x0)
    .attr("y", plot.y0)
    .attr("width", plot.width)
    .attr("height", plot.height);

  // ------------------------------------------------------------
  // Contour generation
  // ------------------------------------------------------------
  const nx = 180;
  const ny = 180;

  const termValues = d3.range(nx).map(i =>
    termDomain[0] + i * (termDomain[1] - termDomain[0]) / (nx - 1)
  );

  const rateValues = d3.range(ny).map(j =>
    rateDomain[1] - j * (rateDomain[1] - rateDomain[0]) / (ny - 1)
  );

  const contourGenerator = d3.contours()
    .size([nx, ny])
    .smooth(true);

  function niceContourLevels(principal) {
    const minPay = monthlyPayment(principal, rateDomain[0], termDomain[1]);
    const maxPay = monthlyPayment(principal, rateDomain[1], termDomain[0]);

    const step = chooseStep(maxPay - minPay);

    const start = Math.ceil(minPay / step) * step;
    const end = Math.floor(maxPay / step) * step;

    return d3.range(start, end + step * 0.5, step);
  }

  function chooseStep(range) {
    if (range <= 1500) return 100;
    if (range <= 3000) return 250;
    if (range <= 8000) return 500;
    if (range <= 15000) return 1000;
    return 2000;
  }

  function gridValues(principal) {
    const values = [];

    for (let j = 0; j < ny; j++) {
      for (let i = 0; i < nx; i++) {
        values.push(
          monthlyPayment(principal, rateValues[j], termValues[i])
        );
      }
    }

    return values;
  }

  function contourPath(d) {
    const path = d3.geoPath(
      d3.geoTransform({
        point: function(px, py) {
          const term = termDomain[0] + px * (termDomain[1] - termDomain[0]) / (nx - 1);
          const rate = rateDomain[1] - py * (rateDomain[1] - rateDomain[0]) / (ny - 1);
          this.stream.point(xTerm(term), yRate(rate));
        }
      })
    );

    return path(d);
  }

  function drawContours(principal) {
    const values = gridValues(principal);
    const levels = niceContourLevels(principal);

    const contours = contourGenerator
      .thresholds(levels)(values);

    contourLayer.selectAll("*").remove();
    labelLayer.selectAll("*").remove();

    contourLayer.selectAll("path")
      .data(contours)
      .enter()
      .append("path")
      .attr("class", d => d.value % (levels[1] - levels[0] === 0 ? 1 : 2 * (levels[1] - levels[0])) === 0
        ? "contour major"
        : "contour"
      )
      .attr("d", contourPath);

    // Place labels near the right side by solving for rate at a chosen term.
    // This avoids clutter and keeps labels readable.
    contours.forEach((c, idx) => {
      const repayment = c.value;

      // Try a few label positions; keep the first valid one.
      const candidateTerms = [31, 27, 23, 19, 15, 11];

      let label = null;

      for (const term of candidateTerms) {
        const rate = solveRateForPayment(principal, term, repayment);

        if (rate !== null && rate >= rateDomain[0] && rate <= rateDomain[1]) {
          const x = xTerm(term);
          const y = yRate(rate);

          // Estimate local angle using neighboring term.
          const rate2 = solveRateForPayment(principal, term + 0.4, repayment);
          let angle = 0;

          if (rate2 !== null) {
            const x2 = xTerm(term + 0.4);
            const y2 = yRate(rate2);
            angle = Math.atan2(y2 - y, x2 - x) * 180 / Math.PI;
          }

          label = { x, y, angle };
          break;
        }
      }

      if (label) {
        labelLayer.append("text")
          .attr("class", "contour-label")
          .attr("x", label.x)
          .attr("y", label.y - 4)
          .attr("text-anchor", "middle")
          .attr("transform", `rotate(${label.angle},${label.x},${label.y})`)
          .text(fmtMoney(repayment));
      }
    });
  }

  function solveRateForPayment(principal, term, targetPayment) {
    let lo = rateDomain[0];
    let hi = rateDomain[1];

    const fLo = monthlyPayment(principal, lo, term);
    const fHi = monthlyPayment(principal, hi, term);

    if (targetPayment < fLo || targetPayment > fHi) {
      return null;
    }

    for (let k = 0; k < 40; k++) {
      const mid = (lo + hi) / 2;
      const fMid = monthlyPayment(principal, mid, term);

      if (fMid < targetPayment) {
        lo = mid;
      } else {
        hi = mid;
      }
    }

    return (lo + hi) / 2;
  }

  // ------------------------------------------------------------
  // Selected contour
  // ------------------------------------------------------------
  function drawSelectedContour(principal, selectedPayment) {
    selectedContourLayer.selectAll("*").remove();

    const values = gridValues(principal);
    const c = contourGenerator
      .thresholds([selectedPayment])(values)[0];

    if (!c) return;

    selectedContourLayer.append("path")
      .datum(c)
      .attr("class", "repayment-line")
      .attr("d", contourPath);
  }

  // ------------------------------------------------------------
  // Interaction
  // ------------------------------------------------------------
  function setRateTermFromPointer(event) {
    // d3.drag passes a custom drag event.
    // For click events, use the event directly.
    // For drag events, use event.sourceEvent, which is the original mouse/pointer event.
    const source = event.sourceEvent || event;

    // Always compute coordinates relative to the SVG, not relative to the event target.
    const [mx, my] = d3.pointer(source, svg.node());

    const termRaw = xTerm.invert(mx);
    const rateRaw = yRate.invert(my);

    const term = Math.round(clamp(termRaw, termDomain[0], termDomain[1]));
    const rate = Math.round(clamp(rateRaw, rateDomain[0], rateDomain[1]) * 10) / 10;

    termInput.property("value", term);
    rateInput.property("value", rate);

    update(false);
  }

  dragCapture
    .on("click", setRateTermFromPointer)
    .call(
      d3.drag()
        .on("start", setRateTermFromPointer)
        .on("drag", setRateTermFromPointer)
    );

  principalInput.on("input", () => update(true));
  rateInput.on("input", () => update(false));
  termInput.on("input", () => update(false));

  function update(redrawContours) {
    const principal = +principalInput.property("value");
    const rate = +rateInput.property("value");
    const term = +termInput.property("value");

    const payment = monthlyPayment(principal, rate, term);
    const totalPaid = payment * term * 12;
    const totalInterest = totalPaid - principal;

    principalValue.text(fmtMoney(principal));
    rateValue.text(d3.format(".1f")(rate) + "% p.a.");
    termValue.text(term + " years");

    result.html(`
      <strong>Monthly repayment:</strong> ${fmtMoney2(payment)}
      &nbsp;&nbsp;|&nbsp;&nbsp;
      <strong>Total paid:</strong> ${fmtMoney2(totalPaid)}
      &nbsp;&nbsp;|&nbsp;&nbsp;
      <strong>Total interest:</strong> ${fmtMoney2(totalInterest)}
    `);

    if (redrawContours) {
      drawContours(principal);
    }

    drawSelectedContour(principal, payment);

    const x = xTerm(term);
    const y = yRate(rate);

    rateGuide
      .attr("x1", plot.x0)
      .attr("x2", x)
      .attr("y1", y)
      .attr("y2", y);

    termGuide
      .attr("x1", x)
      .attr("x2", x)
      .attr("y1", y)
      .attr("y2", plot.y1);

    selectedPoint
      .attr("cx", x)
      .attr("cy", y);

    selectedLabel
      .attr("x", x + 14)
      .attr("y", y - 14)
      .text(`${d3.format(".1f")(rate)}%, ${term}y → ${fmtMoney2(payment)}`);
  }

  // Initial draw
  drawContours(+principalInput.property("value"));
  update(false);
</script>

<h2 id="how-it-works">How it works</h2>

<p>The contour lines show combinations of <strong>interest rate</strong> and <strong>loan term</strong> that produce the same monthly repayment for the selected loan amount. This makes the trade-off visible: for a fixed loan, a shorter term can have the same repayment effect as a higher interest rate, and vice versa.</p>

<h2 id="the-maths-behind">The maths behind</h2>

<p>The equation for a fixed scheduled repayment \(M\) of a principal loan \(P\) over a number of periods \(n\) with an interest rate \(r\) is</p>

\[M = P \frac{r(1+r)^n}{(1+r)^n-1}.\]

<p>In case of monthly repayments, we divide the annual interest rate (it’s usually given as % p.a., which is short for <em>per annum</em>, which is Latin for per year) by 12 and have to multiply \(n\) with 12 if the duration of the term is in years.</p>

<p>Let’s find out under what conditions the principal equals the interest paid.</p>

<p>The total amount repaid is \(n\,M\).
The total interest is \(I = n\,M - P\).
And we want:</p>

\[I = P\]

<p>A little algebra turns this into</p>

\[n\,M - P = P\]

<p>or:</p>

\[n\,M = 2P\]

<p>We can substitute the amortization formula back for \(M\):</p>

\[n\,P \frac{r(1+r)^n}{(1+r)^n-1} =  2P\]

<p>\(P\) cancels out.
This means our answer doesn’t depend on the principal loan amount \(P\).</p>

<p>We are left with</p>

\[n\frac{r(1+r)^n}{(1+r)^n-1} = 2\]

<p>We can rewrite \(r\) and \(n\) in annual-rates-and-years form.
With \(R\) as the annual interest rate and \(T\) as term in years we have \(r = \frac{R}{12}\) and \(n=12\,T\).
So</p>

\[n\,r = 12\,T \cdot \frac{R}{12} = R\,T\]

<p>The exact condition becomes</p>

\[\frac{R\,T(1+\frac{R}{12})^{12\,T}}{(1+\frac{R}{12})^{12\,T}-1} = 2\]

<p>The quality of principal and interest depends mostly on the product \(R\,T\), or the annual rate \(\times\) years.</p>

<p>We can get a useful heuristic when we replace monthly compounding with annual compounding</p>

\[\left(1+\frac{R}{12}\right)^{-12\,T} \approx e^{-R\,T}\]

<p>Then the condition becomes</p>

\[\frac{R\,T}{1-e^{-R\,T}} = 2\]

<p>Let \(x = R\,T\), then</p>

\[\frac{x}{1-e^{-x}} = 2\]

<p>This equation has the solution \(x \approx 1.594\).
So the mental rule is \(\boxed{\text{annual rate}\;\times\;\text{years} \approx 1.6}\).</p>

<p>Examples:</p>

\[0.053\,\times\,30 \approx 1.59\]

<p>So whenever you find a loan in the wild where the product of annual interest rate times its duration equals 1.6 you know that someone is paying the same amount on interest as their loan is worth.</p>]]></content><author><name>Mario</name></author><summary type="html"><![CDATA[Mortgage calculator]]></summary></entry><entry><title type="html">Investment scenarios</title><link href="/2026/06/19/wealth.html" rel="alternate" type="text/html" title="Investment scenarios" /><published>2026-06-19T00:00:00+00:00</published><updated>2026-06-19T00:00:00+00:00</updated><id>/2026/06/19/wealth</id><content type="html" xml:base="/2026/06/19/wealth.html"><![CDATA[<style>
  .row { margin-bottom: 10px; }
  label { display: inline-block; width: 260px; }
  input[type=range] { width: 250px; }
  .value { display: inline-block; width: 100px; text-align: right; }
  table { border-collapse: collapse; margin-top: 20px; width: 100%; }
  th, td { border: 1px solid #ccc; padding: 8px; text-align: right; }
  th { background: #eee; }
  td:first-child { text-align: left; font-weight: bold; }
</style>

<h2 id="investment">Investment?</h2>

<p>Hi there. If you’re like me, you feel like you want to kick yourself in the butt for not having invested into your own future.
Life, when you’re young, just feels too good to think about any of that. And do you really have any extra cash that you could save?
$50. Would it even matter?</p>

<p>It’s never too late. So why not start today?</p>

<p>The big idea is of course <em>compounding</em>. There are many great shortcomings of the human race, but according to physicist Dr. Albert Bartlett:</p>

<blockquote>
  <p>The greatest shortcoming of the human race is our inability to understand the exponential function.</p>
</blockquote>

<iframe width="560" height="315" src="https://www.youtube.com/embed/1bvwOrGn1Zs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>

<h2 id="investment-scenario-calculator">Investment Scenario Calculator</h2>

<p>Let’s see how far you can get with saving a little extra every year (10%).</p>

<p>Here are a few sliders to play around with.
There are dollar amounts (I live in New Zealand, so don’t read too much into the numbers. 1 NZD today is about USD 0.58, or EUR 0.50) and there are rates.
And there are years (otherwise there wouldn’t be much compounding, would there?)</p>

<p>You can start with zero capital, or with no savings and let only your invested capital compound.</p>

<p>If you know by how much your annual income would grow, you can do change that rate, too.</p>

<p>I’ve also included <em>Uncertainty</em>. It increases or decreases the rates by that much, so we can check out worst and best case scenarios.
(The numbers in brackets show the values for worst- and best-case).</p>

<p>There is a line called <em>Retirement Income</em>.
This is an inflation-adjusted annual lump sum you would receive after tax, if you were to move all your wealth into a term deposit and just live off the interest rates.</p>

<p>The last two lines are sensitivities.
I talk about them <a href="#technical-details">down below</a>.</p>

<p>OK. I’ll leave you to it. Happy investing!!</p>

<div id="inputs"></div>

<p><button onclick="reset()">Reset</button></p>

<table>
  <thead>
    <tr>
      <th></th>
      <th>Worst</th>
      <th>Middle</th>
      <th>Best</th>
    </tr>
  </thead>
  <tbody id="results"></tbody>
</table>

<script>
const config = [
  { id: "K0", label: "Starting Capital \\(K_0\\)", min: 0, max: 500000, step: 1000, value: 50000, pct: false },
  { id: "Y1", label: "Net Year-1 Income \\(Y_1\\)", min: 0, max: 300000, step: 1000, value: 80000, pct: false },
  { id: "s", label: "Savings Rate \\(s\\)", min: 0, max: 50, step: 1, value: 10, pct: true },
  { id: "r", label: "Investment Return Rate \\(r\\)", min: 0, max: 20, step: 0.1, value: 11, pct: true },
  { id: "g", label: "Income Growth Rate \\(g\\)", min: 0, max: 10, step: 0.1, value: 3, pct: true },
  { id: "pi", label: "Inflation Rate \\(\\pi\\)", min: 0, max: 10, step: 0.1, value: 3, pct: true, reverse: true },
  { id: "N", label: "Years \\(N\\)", min: 1, max: 70, step: 1, value: 25, pct: false },
  { id: "d", label: "Deposit Rate \\(d\\)", min: 0, max: 10, step: 0.1, value: 5, pct: true },
  { id: "t", label: "Tax Rate \\(t\\)", min: 0, max: 50, step: 1, value: 30, pct: true, reverse: true },
  { id: "u", label: "Uncertainty", min: 0, max: 50, step: 1, value: 20, pct: true }
];

// Create UI
const container = document.getElementById("inputs");

config.forEach(c => {
  const div = document.createElement("div");
  div.className = "row";

  div.innerHTML = `
    <label>
      <span class="base">${c.label}</span>
      <span id="${c.id}_extra"></span>
    </label>
    <input type="range" id="${c.id}" min="${c.min}" max="${c.max}" step="${c.step}" value="${c.value}">
    <span class="value" id="${c.id}_val"></span>
  `;
  container.appendChild(div);
});

function fmtMoney(x) {
  return "$" + Math.round(x).toLocaleString();
}

function fmtPercent(x) {
  return (x * 100).toFixed(1) + "%";
}

// Scenario calculator
function calcScenario(p) {
  let capital = p.K0;

  for (let year = 0; year < p.N; year++) {
    const income = p.Y1 * Math.pow(1 + p.g, year);
    const contribution = income * p.s;
    capital = capital * (1 + p.r) + contribution;
  }

  const nominal = capital;
  const real = nominal / Math.pow(1 + p.pi, p.N);
  const incomeOut = real * p.d * (1 - p.t);

  return { nominal, real, incomeOut };
}

function calcSensitivity(KN, r, g, Y1, N, s) {
  const YN = Y1 * Math.pow(1 + g, N);

  const deltaYear = KN * r + s * YN;

  const deltaSavings =
    0.01 * Y1 *
    (Math.pow(1 + r, N) - Math.pow(1 + g, N)) /
    (r - g);

  return { deltaYear, deltaSavings };
}

function run() {
  const vals = {};
  config.forEach(c => vals[c.id] = +document.getElementById(c.id).value);

  const U = vals.u / 100;

  const down = x => x * (1 - U);
  const up = x => x * (1 + U);

  function scenarioValue(key, base) {
    const conf = config.find(c => c.id === key);
    if (!conf.pct) return { worst: base, best: base };

    if (conf.reverse) {
      return { worst: up(base), best: down(base) };
    }
    return { worst: down(base), best: up(base) };
  }

  const inputs = {
    K0: vals.K0,
    Y1: vals.Y1,
    N: vals.N,
    s: vals.s / 100,
    r: vals.r / 100,
    g: vals.g / 100,
    pi: vals.pi / 100,
    d: vals.d / 100,
    t: vals.t / 100
  };

  const worst = {}, middle = {}, best = {};

  Object.keys(inputs).forEach(k => {
    if (["K0","Y1","N", "s"].includes(k)) {
      worst[k] = middle[k] = best[k] = inputs[k];
    } else {
      const { worst: w, best: b } = scenarioValue(k, inputs[k]);
      worst[k] = w;
      middle[k] = inputs[k];
      best[k] = b;
    }
  });

  config.forEach(c => {
    const val = vals[c.id];
    const valSpan = document.getElementById(c.id + "_val");
    const extra = document.getElementById(c.id + "_extra");

    if (c.id === "N") {
      valSpan.textContent = val.toFixed(0);
    } else {
      valSpan.textContent = c.pct ? val.toFixed(1) + "%" : fmtMoney(val);
    }

    if (c.pct && !["u", "s"].includes(c.id)) {
      const { worst: w, best: b } = scenarioValue(c.id, val / 100);
      extra.textContent = ` (${fmtPercent(w)} / ${fmtPercent(b)})`;
    } else {
      extra.textContent = "";
    }
  });

  const w = calcScenario(worst);
  const m = calcScenario(middle);
  const b = calcScenario(best);

  const sens = calcSensitivity(m.real, inputs.r, inputs.g, inputs.Y1, inputs.N, inputs.s);

  document.getElementById("results").innerHTML = `
    <tr><td>Nominal Capital <i>K<sub>nom</sub></i></td><td>${fmtMoney(w.nominal)}</td><td>${fmtMoney(m.nominal)}</td><td>${fmtMoney(b.nominal)}</td></tr>
    <tr><td>Real Capital K<sub>real</sub></td><td>${fmtMoney(w.real)}</td><td>${fmtMoney(m.real)}</td><td>${fmtMoney(b.real)}</td></tr>
    <tr><td>Retirement Income</td><td>${fmtMoney(w.incomeOut)}</td><td>${fmtMoney(m.incomeOut)}</td><td>${fmtMoney(b.incomeOut)}</td></tr>
    <tr><td>+1 Year Impact</td><td colspan="3" style="text-align: center;">${fmtMoney(sens.deltaYear)}</td></tr>
    <tr><td>+1% Savings Impact</td><td colspan="3" style="text-align: center;">${fmtMoney(sens.deltaSavings)}</td></tr>
  `;
}

// Reset button
function reset() {
  config.forEach(c => {
    document.getElementById(c.id).value = c.value;
  });
  update();
}

function update() {
  run();
}

// Event listeners
config.forEach(c => {
  document.getElementById(c.id).addEventListener("input", update);
});

// Initial run
update();
</script>

<h2 id="technical-details">Technical Details</h2>

<p>This is the basic formula for everything above.</p>

\[K_{nom} = K_0 (1+r)^N + s \cdot Y_1 \cdot \frac{(1+r)^N - (1+g)^N}{r - g}\]

<p>The nominal captial is quite large. That’s because we haven’t adjusted it for inflation.
Dividing the nominal capital by the total inflation over N years, gives us the real capital, in terms of today’s purchasing power.</p>

\[K_{real} = K_{nom} / (1+\pi)^N\]

<p>I was also curious about how tiny adjustments in behaviour might lead to bigger savings.
There are two things we can control: savings rate and extra years.</p>

<p>Mathematically, the sensitivities can be calculated using <a href="https://en.wikipedia.org/wiki/Derivative" target="_blank">derivates</a>.
Taking the derivative of \(K_{nom}\) (or \(K_{real}\)) with respect to \(s\) gives us the sensitivity to changing the savings rate.</p>

\[\frac{dK_{nom}}{ds} = Y_1 \cdot \frac{(1+r)^N - (1+g)^N}{r - g}\]

<p>You’ve probably seen that some sliders don’t make a difference in the <em>+1% Savings Impact</em>.
Now, you can look for yourself why that is. (Yes. LOOK AT THE FORMULA!!!)</p>

<p>Astonishingly, the savings rate itself doesn’t make a difference. Why?
Because every additional 1 percentage point of savings buys you the same chunk of lifetime wealth, regardless of whether you’re already saving 2% or 40%.</p>

<p>In real life, you also don’t have much control about \(Y_1\), \(r\), \(g\); all variables that determine the sensitivity.
But you have control of \(N\), the time you let your wealth accumulate.</p>

<p>Let’s look at the sensitivity of saving for another year.</p>

\[\frac{dK_{nom}}{dN} = K_0 (1+r)^N \ln(1+r) + s \cdot Y_1 \cdot \frac{ (1+r)^N \ln(1+r) - (1+g)^N \ln(1+g) }{r - g}\]

<p>I know, there is a lot going on here.
But we can break it up and look into the two terms of the above sum.</p>

<p>The first term is related to your starting capital compounding over the years (\(K_0 (1+r)^N\)) and it will simply grow some more for another year.
If you dial the slider of your year-1 income down to 0, you can see that the <em>+1 Year Impact</em> is the <em>Real Capital</em> times the <em>Investment Return Rate</em>.
An extra year is valuable because it applies your return rate to your entire accumulated financial history.</p>

<p>The second term is related to your savings.
It’s a bit too complicated to explain it in simple terms (and there is a minus in the numerator of the fraction).
But you can think of it as growth on past savings (\((1+r)^N \ln(1+r)\)) that is adjusted for income growth (\(-(1+g)^N \ln(1+g)\)).</p>

<p>To sum up all of the details about the sensitivities, just keep in mind</p>

<blockquote>
  <p>Savings rate controls <strong>how much fuel you add</strong>, but time controls <strong>how long it burns</strong>.</p>
</blockquote>]]></content><author><name>Mario</name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Notan</title><link href="/2025/10/21/notan.html" rel="alternate" type="text/html" title="Notan" /><published>2025-10-21T00:00:00+00:00</published><updated>2025-10-21T00:00:00+00:00</updated><id>/2025/10/21/notan</id><content type="html" xml:base="/2025/10/21/notan.html"><![CDATA[<h1 id="notan-for-values-exercises">Notan for values exercises</h1>

<p><a href="https://en.wikipedia.org/wiki/Notan" target="_blank">Notan</a> is a Japanese word that is used in arts to describe the balance of light and shade. As a visual aid, Notan helps us seeing an image as value shapes, not as contour lines.</p>

<p>This tool is insipred by the <a href="https://www.proko.com/values" target="_blank">Proko’s values tool</a>.</p>

<p><input id="file" type="file" accept="image/*" /><br />
<label>Blur: <input id="blurRange" type="range" min="0" max="8" step="1" value="2" /></label>
<label>Levels: <input id="levelsRange" type="range" min="2" max="6" step="1" value="3" /></label>
<button id="download">Download PNG</button>
<br /><br /></p>
<canvas id="output"></canvas>
<canvas id="source" style="display:none"></canvas>
<canvas id="histogram" width="512" height="150"></canvas>
<div id="sliders"></div>

<script>
const fileEl = document.getElementById('file');
const blurRange = document.getElementById('blurRange');
const levelsRange = document.getElementById('levelsRange');
const output = document.getElementById('output');
const source = document.getElementById('source');
const histogramCanvas = document.getElementById('histogram');
const slidersDiv = document.getElementById('sliders');
const downloadBtn = document.getElementById('download');

let img = new Image();
let thresholds = [];

fileEl.addEventListener('change', handleFile);
blurRange.addEventListener('input', render);
levelsRange.addEventListener('input', ()=>{generateThresholdSliders(); render();});
downloadBtn.addEventListener('click', downloadPNG);

function handleFile(e){
  const f = e.target.files && e.target.files[0];
  if(!f) return;
  const url = URL.createObjectURL(f);
  img = new Image();
  img.onload = ()=>{ URL.revokeObjectURL(url); render(); };
  img.src = url;
}

function generateThresholdSliders(){
  slidersDiv.innerHTML = '';
  const levels = parseInt(levelsRange.value);
  thresholds = [];
  for(let i=0;i<levels-1;i++){
    const slider = document.createElement('input');
    slider.type = 'range';
    slider.min = 0; slider.max = 255; slider.value = Math.round(255*(i+1)/levels);
    slider.dataset.index = i;
    slider.addEventListener('input', ()=>{ thresholds[i] = parseInt(slider.value); render(); });
    slidersDiv.appendChild(document.createTextNode(`Threshold ${i+1}: `));
    slidersDiv.appendChild(slider);
    slidersDiv.appendChild(document.createElement('br'));
    thresholds.push(parseInt(slider.value));
  }
}

// Simple box blur (works on mobile)
function blurCanvas(canvas, radius = 2, iterations = 2) {
  const ctx = canvas.getContext('2d');
  let { width, height } = canvas;
  let imageData = ctx.getImageData(0, 0, width, height);
  let data = imageData.data;
  const tmp = new Uint8ClampedArray(data);

  for (let iter = 0; iter < iterations; iter++) {
    // Horizontal pass
    for (let y = 0; y < height; y++) {
      let offset = y * width * 4;
      for (let x = 0; x < width; x++) {
        let r = 0, g = 0, b = 0, count = 0;
        for (let dx = -radius; dx <= radius; dx++) {
          const ix = x + dx;
          if (ix >= 0 && ix < width) {
            const i = offset + ix * 4;
            r += tmp[i];
            g += tmp[i + 1];
            b += tmp[i + 2];
            count++;
          }
        }
        const i = offset + x * 4;
        data[i] = r / count;
        data[i + 1] = g / count;
        data[i + 2] = b / count;
      }
    }

    tmp.set(data); // Copy back

    // Vertical pass
    for (let x = 0; x < width; x++) {
      for (let y = 0; y < height; y++) {
        let r = 0, g = 0, b = 0, count = 0;
        for (let dy = -radius; dy <= radius; dy++) {
          const iy = y + dy;
          if (iy >= 0 && iy < height) {
            const i = (iy * width + x) * 4;
            r += tmp[i];
            g += tmp[i + 1];
            b += tmp[i + 2];
            count++;
          }
        }
        const i = (y * width + x) * 4;
        data[i] = r / count;
        data[i + 1] = g / count;
        data[i + 2] = b / count;
      }
    }

    tmp.set(data);
  }

  ctx.putImageData(imageData, 0, 0);
}

generateThresholdSliders();

function render(){
  if(!img || !img.complete || img.naturalWidth===0) return;
  const blur = parseInt(blurRange.value,10);
  const levels = Math.max(2, parseInt(levelsRange.value,10));
  if(thresholds.length!==levels-1) generateThresholdSliders();

  output.width = img.naturalWidth;
  output.height = img.naturalHeight;
  source.width = img.naturalWidth;
  source.height = img.naturalHeight;

  const sctx = source.getContext('2d');
  const octx = output.getContext('2d');
  sctx.filter = 'none';
  sctx.drawImage(img, 0, 0, source.width, source.height);

  // Apply custom blur after drawing
  if (blur > 0) {
    blurCanvas(source, blur); // our JS blur filter
  }

  const imgData = sctx.getImageData(0,0,source.width,source.height);
  const data = imgData.data;
  const hist = new Array(256).fill(0);

  for(let i=0;i<data.length;i+=4){
    const r = data[i], g = data[i+1], b = data[i+2];
    let lum = 0.2126*r + 0.7152*g + 0.0722*b;
    hist[Math.floor(lum)]++;
  }

  drawHistogram(hist);

  // Posterize using thresholds
  const sortedT = [...thresholds].sort((a,b)=>a-b);
  for(let i=0;i<data.length;i+=4){
    const r = data[i], g = data[i+1], b = data[i+2];
    let lum = 0.2126*r + 0.7152*g + 0.0722*b;
    let level = 0;
    while(level < sortedT.length && lum > sortedT[level]) level++;
    const v = Math.round(255 * level / (levels-1));
    data[i]=data[i+1]=data[i+2]=v;
  }
  octx.putImageData(imgData,0,0);
}

function drawHistogram(hist){
  const ctx = histogramCanvas.getContext('2d');
  ctx.clearRect(0,0,histogramCanvas.width,histogramCanvas.height);
  const max = Math.max(...hist);
  const w = histogramCanvas.width / hist.length;
  for(let i=0;i<hist.length;i++){
    const h = (hist[i]/max)*histogramCanvas.height;
    ctx.fillStyle = '#999';
    ctx.fillRect(i*w, histogramCanvas.height-h, w, h);
  }
  // Draw threshold lines
  ctx.strokeStyle='red';
  ctx.lineWidth=1;
  thresholds.forEach(t=>{
    ctx.beginPath();
    const x = t/255*histogramCanvas.width;
    ctx.moveTo(x,0);ctx.lineTo(x,histogramCanvas.height);
    ctx.stroke();
  });
}

function downloadPNG(){
  const link = document.createElement('a');
  link.download = 'notan.png';
  link.href = output.toDataURL('image/png');
  link.click();
}
</script>]]></content><author><name>Mario Krapp (© 2013-2025)</name><email>mariokrapp@gmail.com</email></author><summary type="html"><![CDATA[Notan for values exercises]]></summary></entry><entry><title type="html">Maze Generator</title><link href="/2025/10/13/maze.html" rel="alternate" type="text/html" title="Maze Generator" /><published>2025-10-13T00:00:00+00:00</published><updated>2025-10-13T00:00:00+00:00</updated><id>/2025/10/13/maze</id><content type="html" xml:base="/2025/10/13/maze.html"><![CDATA[<p>I love mazes.</p>

<p>They can be fun.</p>

<p>They can be hard.</p>

<p>And they can be educational.</p>

<p>The maze (generator) below is using <a href="https://en.wikipedia.org/wiki/Kruskal%27s_algorithm" target="_blank"><em>Kruskal’s Algorithm</em></a> and it is inspired by <a href="https://professor-l.github.io/mazes/" target="_blank">Professor L.’s Maze Generation Algorithms - An Exploration</a>. You should check out her other mazes generation algorithms and corresponding explainers.</p>

<p>
Size: <input type="number" id="size" value="20" min="5" max="50" />
</p>
<p><button id="generate">Generate Maze</button></p>

<canvas id="mazeCanvas"></canvas>

<p><button id="save">Save as PNG</button></p>

<script>
const canvas = document.getElementById('mazeCanvas');
const ctx = canvas.getContext('2d');
let cellSize = 20;

// ----------------- Kruskal's Maze Generator -----------------
class Cell {
    constructor(row, col) {
        this.row = row;
        this.col = col;
        this.walls = { top: true, right: true, bottom: true, left: true };
        this.set = null;
    }
}

function generateMazeGrid(size) {
    const grid = [];
    let setId = 0;
    for (let r = 0; r < size; r++) {
        const row = [];
        for (let c = 0; c < size; c++) {
            const cell = new Cell(r, c);
            cell.set = setId++;
            row.push(cell);
        }
        grid.push(row);
    }

    const walls = [];
    for (let r = 0; r < size; r++) {
        for (let c = 0; c < size; c++) {
            if (r > 0) walls.push({cell1: grid[r][c], cell2: grid[r-1][c]});
            if (c > 0) walls.push({cell1: grid[r][c], cell2: grid[r][c-1]});
        }
    }
    return {grid, walls};
}

function removeWallBetween(cell1, cell2) {
    if (cell1.row === cell2.row) {
        if (cell1.col < cell2.col) { cell1.walls.right = false; cell2.walls.left = false; }
        else { cell1.walls.left = false; cell2.walls.right = false; }
    } else {
        if (cell1.row < cell2.row) { cell1.walls.bottom = false; cell2.walls.top = false; }
        else { cell1.walls.top = false; cell2.walls.bottom = false; }
    }
}

// ----------------- Draw full grid -----------------
function drawGrid(grid, size) {
    canvas.width = size * cellSize;
    canvas.height = size * cellSize;
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.strokeStyle = 'black';
    ctx.lineWidth = 2;

    for (let r = 0; r < size; r++) {
        for (let c = 0; c < size; c++) {
            const cell = grid[r][c];
            const x = c * cellSize;
            const y = r * cellSize;

            if (cell.walls.top) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x+cellSize, y); ctx.stroke(); }
            if (cell.walls.right) { ctx.beginPath(); ctx.moveTo(x+cellSize, y); ctx.lineTo(x+cellSize, y+cellSize); ctx.stroke(); }
            if (cell.walls.bottom) { ctx.beginPath(); ctx.moveTo(x, y+cellSize); ctx.lineTo(x+cellSize, y+cellSize); ctx.stroke(); }
            if (cell.walls.left) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y+cellSize); ctx.stroke(); }
        }
    }
}

// ----------------- Animate wall removal -----------------
async function removeWallAnimation(cell1, cell2) {
    const x1 = cell1.col * cellSize;
    const y1 = cell1.row * cellSize;
    const x2 = cell2.col * cellSize;
    const y2 = cell2.row * cellSize;

    ctx.strokeStyle = 'white';
    ctx.lineWidth = 2;

    if (cell1.row === cell2.row) {
        const minCol = Math.min(cell1.col, cell2.col);
        const y = cell1.row * cellSize;
        ctx.beginPath();
        ctx.moveTo((minCol + 1) * cellSize, y);
        ctx.lineTo((minCol + 1) * cellSize, y + cellSize);
        ctx.stroke();
    } else {
        const minRow = Math.min(cell1.row, cell2.row);
        const x = cell1.col * cellSize;
        ctx.beginPath();
        ctx.moveTo(x, (minRow + 1) * cellSize);
        ctx.lineTo(x + cellSize, (minRow + 1) * cellSize);
        ctx.stroke();
    }

    ctx.strokeStyle = 'black';
    await new Promise(resolve => setTimeout(resolve, 1));
}

// ----------------- Generate Maze -----------------
async function generateMaze(size) {
    const {grid, walls} = generateMazeGrid(size);

    drawGrid(grid, size);

    // Shuffle walls
    for (let i = walls.length-1; i>0; i--){
        const j = Math.floor(Math.random()*(i+1));
        [walls[i], walls[j]] = [walls[j], walls[i]];
    }

    // Kruskal's algorithm with animation
    for (const wall of walls) {
        const {cell1, cell2} = wall;
        if (cell1.set !== cell2.set) {
            removeWallBetween(cell1, cell2);
            await removeWallAnimation(cell1, cell2);

            const oldSet = cell2.set;
            const newSet = cell1.set;
            for (let r = 0; r < size; r++) {
                for (let c = 0; c < size; c++) {
                    if (grid[r][c].set === oldSet) grid[r][c].set = newSet;
                }
            }
        }
    }

    // Fixed entrance and exit
    grid[0][0].walls.top = false;
    grid[size-1][size-1].walls.bottom = false;

    drawGrid(grid, size);
}

// ----------------- Initial Grid -----------------
const defaultSize = 20;
let grid = generateMazeGrid(defaultSize).grid;
drawGrid(grid, defaultSize);

// ----------------- UI -----------------
document.getElementById('generate').addEventListener('click', async () => {
    const size = parseInt(document.getElementById('size').value);
    await generateMaze(size);
});

document.getElementById('save').addEventListener('click', () => {
    const canvas = document.getElementById('mazeCanvas');
    const image = canvas.toDataURL('image/png');
    // Detect mobile browsers
    const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);

    if (isMobile) {
        // Open the image in a new tab as a standalone <img>
        const newTab = window.open('', '_blank');
        if (newTab) {
            newTab.document.title = 'Maze Image';
            newTab.document.body.style.margin = '0';
            newTab.document.body.style.background = '#000';
            const img = newTab.document.createElement('img');
            img.src = image;
            img.style.display = 'block';
            img.style.width = '100vw';
            img.style.height = 'auto';
            img.style.objectFit = 'contain';
            newTab.document.body.appendChild(img);
        } else {
            alert('Please allow pop-ups to view the image.');
        }
        } else {
        const link = document.createElement('a');
        link.download = 'maze.png';
        link.href = image;
        link.click();
    }
});
</script>]]></content><author><name>Mario</name></author><summary type="html"><![CDATA[I love mazes.]]></summary></entry><entry><title type="html">River Meandering</title><link href="/2025/10/10/meandering.html" rel="alternate" type="text/html" title="River Meandering" /><published>2025-10-10T00:00:00+00:00</published><updated>2025-10-10T00:00:00+00:00</updated><id>/2025/10/10/meandering</id><content type="html" xml:base="/2025/10/10/meandering.html"><![CDATA[<p>Yesterday, I gave a guest lecture about climate change (past, present, future).</p>

<p>As a motivating example, I showed an aerial image that I took during a helicopter flight aorund <a href="https://en.wikipedia.org/wiki/Aoraki_/_Mount_Cook_National_Park" target="_blank">Aoraki / Mount Cook National Park</a> in May.</p>

<p><img src="/assets/aoraki.png" alt="" /></p>

<p>There is so much structure in that image and I thought, “I could come up with an approximation for that river bed!”.</p>

<p>A few hours later (and thanks to ChatGPT), I had a <em>phenomenological</em> model that would describe the main features of the river bed and the flow of the water within.</p>

<p>The toy model below has 3 main parameters, <em>displacement</em>, <em>decay</em>, and <em>max dimension</em>. You can try to match the real world by changing those. <em>water percentage</em> is a visual effect that color-codes a percentage of all lines in a watery color.</p>

<p><span>Seed:</span>
<input id="seedInput" type="number" value="1234" style="width: 100px;" />
<button id="randomizeBtn">Randomize Seed</button></p>

<p><label>
  Displacement
  <input id="dispSlider" type="range" min="0" max="1.2" step="0.05" value="0.3" />
  <span id="dispVal">0.3</span>
</label></p>

<p><label>
  Decay
  <input id="decaySlider" type="range" min="0" max="1.2" step="0.05" value="0.8" />
  <span id="decayVal">0.8</span>
</label></p>

<p><label>
  Max Dimension
  <input id="depthSlider" type="range" min="1" max="10" step="1" value="6" />
  <span id="depthVal">6</span>
</label></p>

<p><label>
  Water Percentage
  <input id="probSlider" type="range" min="0" max="1" step="0.01" value="0.05" />
  <span id="probVal">0.05</span>
</label></p>

<canvas id="riverCanvas"></canvas>

<script>
const canvas = document.getElementById("riverCanvas");
const ctx = canvas.getContext("2d");

// === Seeded RNG ===
function mulberry32(seed) {
  return function() {
    let t = seed += 0x6D2B79F5;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

let baseSeed = 1234;
let random = mulberry32(baseSeed);
function reseed() {
  random = mulberry32(baseSeed);
}
function randn() {
  let u = 0, v = 0;
  while (u === 0) u = random();
  while (v === 0) v = random();
  return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}

function toCanvasCoords(x, y) {
  return [x / 10 * canvas.width, canvas.height - (y / 5 * canvas.height)];
}

function midpointMeander(start, end, depth, displacement, decay) {
  function subdivide(pts, level, disp) {
    if (level === 0) return pts;
    const newPts = [pts[0]];
    for (let i = 0; i < pts.length - 1; i++) {
      const a = pts[i];
      const b = pts[i + 1];
      const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
      const dir = [b[0] - a[0], b[1] - a[1]];
      const length = Math.hypot(dir[0], dir[1]);
      let perp = [0, 0];
      if (length > 0) {
        perp = [-dir[1] / length, dir[0] / length];
      }
      const offsetMag = disp * Math.sqrt(length) * randn();
      const midShifted = [mid[0] + perp[0] * offsetMag, mid[1] + perp[1] * offsetMag];
      newPts.push(midShifted, b);
    }
    return subdivide(newPts, level - 1, disp * decay);
  }
  return subdivide([start, end], depth, displacement);
}

function chaikinSmoothing(points, iterations = 2) {
  let pts = points.slice();
  for (let it = 0; it < iterations; it++) {
    const newPts = [pts[0]];
    for (let i = 0; i < pts.length - 1; i++) {
      const p0 = pts[i], p1 = pts[i + 1];
      const q = [0.75 * p0[0] + 0.25 * p1[0], 0.75 * p0[1] + 0.25 * p1[1]];
      const r = [0.25 * p0[0] + 0.75 * p1[0], 0.25 * p0[1] + 0.75 * p1[1]];
      newPts.push(q, r);
    }
    newPts.push(pts[pts.length - 1]);
    pts = newPts;
  }
  return pts;
}

function drawRiver(points, color, alpha, width) {
  ctx.beginPath();
  const [x0, y0] = toCanvasCoords(points[0][0], points[0][1]);
  ctx.moveTo(x0, y0);
  for (let i = 1; i < points.length; i++) {
    const [x, y] = toCanvasCoords(points[i][0], points[i][1]);
    ctx.lineTo(x, y);
  }
  ctx.strokeStyle = color;
  ctx.globalAlpha = alpha;
  ctx.lineWidth = width;
  ctx.stroke();
  ctx.globalAlpha = 1.0;
}

function drawRivers(displacement, decay, prob, maxDepth) {
  reseed(); // reset RNG for deterministic behavior
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "rgb(221, 204, 174)";
  //ctx.fillStyle = "palegoldenrod";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  //const c_water = "paleturquoise";
  //const c_bed = "slategrey";
  const c_water = "rgb(190, 219, 215)";
  const c_bed = "rgb(117, 125, 127)";

  const nRivers = 200;
  const rivers = [];

  // Generate river data first
  for (let i = 0; i < nRivers; i++) {
    const depth = Math.floor(random() * (maxDepth - 1)) + 2; // 2–maxDepth
    const start = [random(), 0];
    const end = [10, 4 + random()];
    const river = midpointMeander(start, end, depth, displacement, decay);
    const riverSmooth = chaikinSmoothing(river, 2);
    const width = random() * 10;

    rivers.push({ riverSmooth, depth, width });
  }

  // Determine how many should be water based on probability
  const k = Math.floor(nRivers * prob);

  // Draw first n-k rivers as bed (grey)
  for (let i = 0; i < nRivers - k; i++) {
    const { riverSmooth, width } = rivers[i];
    const alpha = 0.1 + random() * 0.5; // bed alpha
    drawRiver(riverSmooth, c_bed, alpha, width);
  }

  // Draw last k rivers as water (on top)
  for (let i = nRivers - k; i < nRivers; i++) {
    const { riverSmooth, width } = rivers[i];
    const alpha = 1;//0.7 + random() * 0.2; // water alpha
    drawRiver(riverSmooth, c_water, alpha, width);
  }
}


// === UI Controls ===
const dispSlider = document.getElementById("dispSlider");
const decaySlider = document.getElementById("decaySlider");
const probSlider = document.getElementById("probSlider");
const depthSlider = document.getElementById("depthSlider");
const seedInput = document.getElementById("seedInput");
const randomizeBtn = document.getElementById("randomizeBtn");

const dispVal = document.getElementById("dispVal");
const decayVal = document.getElementById("decayVal");
const probVal = document.getElementById("probVal");
const depthVal = document.getElementById("depthVal");

function update() {
  const disp = parseFloat(dispSlider.value);
  const decay = parseFloat(decaySlider.value);
  const prob = parseFloat(probSlider.value);
  const depth = parseInt(depthSlider.value);
  dispVal.textContent = disp.toFixed(2);
  decayVal.textContent = decay.toFixed(2);
  probVal.textContent = prob.toFixed(2);
  depthVal.textContent = depth;
  drawRivers(disp, decay, prob, depth);
}

// When seed input or randomize button changes seed
seedInput.addEventListener("change", () => {
  baseSeed = parseInt(seedInput.value);
  update();
});
randomizeBtn.addEventListener("click", () => {
  baseSeed = Math.floor(Math.random() * 1e9);
  seedInput.value = baseSeed;
  update();
});

// Sliders trigger redraw
[dispSlider, decaySlider, probSlider, depthSlider].forEach(sl => {
  sl.addEventListener("input", update);
});

// Initial draw
update();
</script>

<h2 id="further-details">Further details</h2>

<p>Rivers naturally form curves, or <a href="https://en.wikipedia.org/wiki/Meander" target="_blank">meanders</a>, because of the way water flows and interacts with the river banks. When water flows around a slight bend, it moves faster on the outer edge and slower on the inner edge. The faster water erodes the outer bank, while the slower water deposits sediment on the inner bank. This feedback makes the bend grow over time, producing the winding patterns we see in meandering rivers, oxbow lakes, and floodplains.</p>

<p>In our simulation, we mimic this process by repeatedly shifting the midpoint of each river segment in a random perpendicular direction. This models the <strong>instability and sideways migration</strong> of real rivers. Mathematically, the river’s centerline \(y(x)\) could be described by a simple equation:</p>

\[\frac{\partial y}{\partial t} = D \frac{\partial^2 y}{\partial x^2} + \eta(x,t)\]

<p>Here, \(D\) represents smoothing effects like natural sediment spreading, and \(\eta(x,t)\) is a random term representing local variations in erosion and deposition.</p>

<p>The <strong>midpoint displacement algorithm</strong> is a simple way to recreate this: each subdivision adds a small random shift, controlled by a displacement parameter \(p\) that gets smaller at finer scales (controlled by decay \(d\)). After that, <strong>Chaikin’s smoothing</strong> spreads out sharp bends, similar to how real rivers naturally diffuse their curves. Together, these steps create winding, natural-looking river shapes that balance the randomness of erosion with the smoothing effects of sediment redistribution.</p>]]></content><author><name>Mario</name></author><summary type="html"><![CDATA[Yesterday, I gave a guest lecture about climate change (past, present, future).]]></summary></entry><entry><title type="html">Coin flip probabilities</title><link href="/2025/08/05/coin_flip.html" rel="alternate" type="text/html" title="Coin flip probabilities" /><published>2025-08-05T00:00:00+00:00</published><updated>2025-08-05T00:00:00+00:00</updated><id>/2025/08/05/coin_flip</id><content type="html" xml:base="/2025/08/05/coin_flip.html"><![CDATA[<h2 id="is-it-a-fair-coin">Is it a fair coin?</h2>

<p>I am going to create an intuitive understanding of Bayes’ Theorem, or Bayesian Inference.
I won’t go into the technical details because, that’s what many intros to Bayesian inference
are doing. Instead, we play and learn.</p>

<p>I will still give you Bayes’ Theorem, so you can try to see how it works it’s magic in the
coin flipping example below. All I’m saying is that
<a href="https://www.youtube.com/watch?v=_NEMHM1wDfI" target="_blank">Bayesian inference is counting</a>.</p>

\[p(\theta|x) = \frac{p(x|\theta)p(\theta)}{p(x)}\]

<p>The term on the left \(p(\theta|x)\) is the posterior - How probable is the parameter \(\theta\) given the data \(x\).</p>

<p>The terms on the right are the likelihood \(p(x|\theta)\) - How probable are the data \(x\) given the parameter \(\theta\)?</p>

<p>The prior \(p(\theta)\) - How probable is the parameter \(\theta\) without seeing any data \(x\)?.</p>

<p>And, lastly, the evidence \(p(x)\) - How probable is it to observe the data \(x\)?</p>

<p>Now, let’s forget that this means anything. The only thing we need to know is that when tails (<code class="language-plaintext highlighter-rouge">T</code>) comes up,
the probability distribution function \(p(T) = 2-2x\) and \(x \in {0,1}\).
That’s a linear function that is 2 at \(x=0\) and 1 at \(x=1\).
It’s also a right triangle <em>and</em> a proper probability distribution function because \((2\times1)/2 = 1\) is the right triangle’s area, which is also the area under the curve.
You can see it by typing in <code class="language-plaintext highlighter-rouge">T</code>.</p>

<p>Now, for heads, it’s similar. The probability of <code class="language-plaintext highlighter-rouge">H</code> is \(p(H) = 2x\).
That’s a linear function that is 1 at \(x=0\) and 2 at \(x=1\).
Again, it’s both a right triangle and a proper probability distribution function.
You can see it by typing in <code class="language-plaintext highlighter-rouge">H</code>.</p>

<p>OK. What happens next feels like magic: When you enter multiple coin flip results (e.g., <code class="language-plaintext highlighter-rouge">TT</code>, or <code class="language-plaintext highlighter-rouge">HT</code>, or <code class="language-plaintext highlighter-rouge">HTHHT...</code>),
the probability turns into a more recognizable probability distribution. Wow!
Why is that? It is simple (and repeated) multiplication of those triangles.
Really! (You can check the source code of this site and look for <code class="language-plaintext highlighter-rouge">f_head(x)</code> and <code class="language-plaintext highlighter-rouge">f_tail(x)</code>.</p>

<p>So, what’s going on? We start from the prior belief that both heads and tails are equally likely: a uniform prior distribution.
If we observe tails, we multiply our prior with the “tails triangle”.
This now becomes our new prior and with each subsequent coin flip, we keep updating our posterior based on previous evidence.</p>

<p>That’s what I mean by counting. You just multiply it by what you see (kinda).</p>

<p>(I haven’t talked about evidence \(p(x)\) and I won’t.)</p>

<p>So have a go yourself.
Below is an input form where you can enter your coin flips. Starting form a uniform
distribution of heads (<code class="language-plaintext highlighter-rouge">H</code>, on the right) and tails (<code class="language-plaintext highlighter-rouge">T</code>, on the left), you can see how
the posterior is updated based on the new data coming in, i.e., the evidence.</p>

<hr />

<p>Enter sequence of H and T:</p>
<p><input id="sequence" placeholder="e.g. HHTHT" /></p>
<p></p>
<canvas id="canvas" width="600" height="300"></canvas>
<div id="stats"></div>

<script>
const statsDiv = document.getElementById("stats");
// Define functions f_init(x), f_head(x), f_tail(x)
function f_init(x) {
    return 0.5; // start with 0.5 everywhere (uniform)
}

function f_head(x) {
    return 2*x;
}

function f_tail(x) {
    return 2-2*x;
}

function update() {
    const input = document.getElementById("sequence").value.trim().toUpperCase();
    // Count Hs and Ts
    const hCount = (input.match(/H/g) || []).length;
    const tCount = (input.match(/T/g) || []).length;
    const total = hCount + tCount;
    const pH = total > 0 ? (hCount / total).toFixed(2) : '?';

    statsDiv.innerText = `H: ${hCount}, T: ${tCount},  p(H): ${pH}`;
    if (!/^[HT]*$/.test(input)) {
        alert("Only H and T characters are allowed.");
        return;
    }

    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext("2d");
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Sample x values (0 to 1)
    const N = 500;
    const xs = Array.from({ length: N }, (_, i) => i / (N - 1));
    let ys = xs.map(x => f_init(x));

    // Apply the H/T sequence
    for (const symbol of input) {
        for (let i = 0; i < xs.length; i++) {
            if (symbol === 'H') {
                ys[i] *= f_head(xs[i]);
            } else if (symbol === 'T') {
                ys[i] *= f_tail(xs[i]);
            }
        }
    }

    // Find max for scaling
    const maxY = Math.max(...ys) || 1;

    const marginTop = 10;
    const marginBottom = 10;
    const xDashed = 0.5;
    const xPix = xDashed * canvas.width;
    ctx.strokeStyle = 'red';
    ctx.lineWidth = 1;
    ctx.setLineDash([5, 5]); // dash 5px, gap 5px
    ctx.beginPath();
    ctx.moveTo(xPix, marginTop);
    ctx.lineTo(xPix, canvas.height - marginBottom);
    ctx.stroke();
    ctx.setLineDash([]); // reset dash pattern

    // Plot function
    ctx.strokeStyle = "black";
    ctx.lineWidth = 2;
    ctx.beginPath();
    for (let i = 0; i < xs.length; i++) {
        const xPix = xs[i] * canvas.width;
        const yPix = canvas.height - marginBottom - (ys[i] / maxY) * (canvas.height - 2*marginBottom);
        if (i === 0) {
            ctx.moveTo(xPix, yPix);
        } else {
            ctx.lineTo(xPix, yPix);
        }
    }
    ctx.stroke();
}
document.getElementById("sequence").addEventListener("input", update);
</script>]]></content><author><name>Mario</name></author><summary type="html"><![CDATA[Is it a fair coin?]]></summary></entry><entry><title type="html">The data generating process</title><link href="/2024/11/06/data_generating_process.html" rel="alternate" type="text/html" title="The data generating process" /><published>2024-11-06T00:00:00+00:00</published><updated>2024-11-06T00:00:00+00:00</updated><id>/2024/11/06/data_generating_process</id><content type="html" xml:base="/2024/11/06/data_generating_process.html"><![CDATA[<p>Hello. If you, like me, are looking at data all day every day (or just sometimes), you want to start thinking about <em>The Data Generating Process</em>. According to one of the shortest articles I’ve seen on <a href="https://en.wikipedia.org/wiki/Data_generating_process">Wikipedia</a></p>
<blockquote>
  <p>a <strong>data generating process</strong> is a process in the real world that “generates” the data one is interested in.</p>
</blockquote>

<p>Thinking about the data generating process has been a game changer for me.</p>

<p>Before getting into the weeds, let’s unpack what <em>data generating process</em> means and how you can can use it as a mental model. If we flip <em>data generating process</em> around, we get the sentence:</p>

<p><strong>Processes generate data.</strong></p>

<ol>
  <li><a href="https://www.websters1913.com/words/Process"><strong>Process</strong></a> is “a series of actions, motions, or occurrences”.</li>
  <li><a href="https://www.websters1913.com/words/Generate"><strong>Generate</strong></a> means “to bring to life”.</li>
  <li><a href="https://www.websters1913.com/words/Datum"><strong>Data</strong></a> (plural for datum) are “facts or principles granted”</li>
</ol>

<p>I like those old dictionary definitions. <a href="http://jsomers.net/blog/dictionary">Webster’s 1913</a> is particularly rewarding in its essence and clarity. Using our new words (or old?), let’s create a more vivid description of <em>data generating process</em>:</p>

<p><strong>A series of actions, motions, or occurrences bring to life facts.</strong></p>

<p>Splendid!</p>

<p>Isn’t that how we think the world keeps going? I think so, too: Something is going on, and “poof!”, there’s an outcome, and when we look at it we try to make sense about what was causing the outcome. What is that Something? And that Something is the (<em>data generating</em>) <em>process</em>.</p>

<p>Let’s say you are looking at a series of data points, for example, a time series. Depending on the domain, you might describe what you see as follows:</p>

<ul>
  <li>“The numbers go up and down a lot, but over time you can see which way things are moving.” (Election Polls)</li>
  <li>“There’s a general upward trend, but with occasional periods of decline.” (Stock Market)</li>
  <li>“Over time, things are getting warmer, though there are still some cooler periods.” (Climate Change)</li>
  <li>“The trend is upward, but sales can vary a lot from month to month.” (Retail Sales)</li>
</ul>

<h1 id="fact-number-one-looking-at-data-makes-you-think-about-the-what">Fact number one: Looking at data makes you think about the <em>What</em>.</h1>

<p>That’s good but not good enough. You can do better. You want to understand “Why” things are the way they are and “How” they came to be.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> You are describing that facts but you are not (yet) looking into what kinds of actions have brought those facts to life. If you were to think about <em>actions, motions, or occurrences</em>, you start thinking about what those actions, motions, or occurences would give rise to.</p>

<p>I look at data a lot. Being a climate data scientist, I am curious about the past, the present, and the future of the climate system. Climate science, as a discipline, is rich in data. There are about 17,500 stations around the world that record weather data (<a href="https://wmo.int/activities/global-observing-system-gos/global-observing-system-gos">source</a>). There are currently 322 earth observation satellites in orbit around Earth (<a href="https://wmo.int/topics/earth-observation-satellites">source</a>). We do that because</p>

<blockquote>
  <p>Accurate weather forecast and climate prediction are crucial for decision making and support for appropriate action to mitigate the impacts of natural hazards and climate change. - <a href="https://wmo.int">WMO</a></p>
</blockquote>

<p>Weather forecast goes beyond looking at data. And that brings us to fact number two.</p>

<h1 id="fact-number-two-generating-data-makes-you-think-about-the-how">Fact number two: Generating data makes you think about the <em>How</em>.</h1>

<p>Here, by data I mean fake data<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>. Yes, I am saying that you should create fake data. Everyone should. You might actually learn something. Creating fake data is not easy. Particularly, if you want to make your fake data look real. Here’s a challenge.</p>

<p>Try to fake the <a href="https://tradingeconomics.com/united-states/stock-market">US stock market data</a> for the last 50 years!</p>

<p><img src="/assets/spx_ind.png" alt="" /></p>

<p>Or, take <a href="https://climate.nasa.gov/vital-signs/global-temperature/?intent=111">Global Temperature</a>. Try faking that.</p>

<p><img src="/assets/GlobalTemp.png" alt="" /></p>

<h2 id="a-brief-history-of-weather-forecasting">A brief history of weather forecasting</h2>

<p>Let’s look at weather forecast again because it’s a prime example of how we got from looking at data to making predictions (about the weather). According to <a href="https://teachersinstitute.yale.edu/curriculum/units/1994/5/94.05.01/2">history</a>, the Greeks where one of the first meteorologists, and Aristotle is considered the founder of meteorology, having written his <a href="https://en.wikipedia.org/wiki/Meteorology_(Aristotle)">“Meteorological”</a> around 340 B.C. Skipping ahed 2200 years and we are witnessing  <a href="https://www.bbc.com/news/magazine-32483678">the birth of modern weather forecasting</a> in the 1860s.</p>

<p>Today, numeral weather prediction models are tools of the trade for meteorologists and climate scientists. <a href="https://ourworldindata.org/weather-forecasts">Weather forecasts have become much more accurate</a>: “A four-day forecast today is as accurate as a one-day forecast 30 years ago.” We all use weather forecast data on a daily basis on our phones. Open your weather app and you are likely to find some information about the data. If I scroll down on my iPhone weather app, for example, I can find a link to <a href="https://support.apple.com/en-nz/105038">weather data</a>. At the bottom of the page, you will find all the data sources the iPhone weather uses. (All of this is fake data, but in the best and most useful way imaginable.)</p>

<p>By the way, predicting the weather is way easier than predicting the stock market. But that’s another topic.</p>

<p>Now, how can you actually generate data? In practice, you can run numerical simulations of the thing you are interested in. That is usually some sort of computer program that will generate some data outputs for you.</p>

<p>Weather forecasting itself is too complicated for this post, so I’ll guide you through a simpler, yet, as insightful, example.</p>

<h2 id="the-coin-toss">The <em>coin toss</em></h2>

<p>One of my favourite examples for everything is the coin toss. Everyone has a coin and can flip it. It’s a real-world process and one that is simple enough to help our understanding.</p>

<p>Depending how many times you flip the coin you generate observations, or data, from that process. You guessed it, the coin toss itself is a <em>data generating process</em>.</p>

<p>Here’s a coin toss example in Python. (You can copy it or download it <a href="/assets/coin_toss.py">here</a>.)</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">random</span>

<span class="k">def</span> <span class="nf">coin_toss</span><span class="p">(</span><span class="n">num_tosses</span><span class="p">,</span><span class="n">p_head</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">random</span><span class="p">.</span><span class="n">choices</span><span class="p">([</span><span class="s">"H"</span><span class="p">,</span> <span class="s">"T"</span><span class="p">],</span> <span class="n">weights</span><span class="o">=</span><span class="p">[</span><span class="n">p_head</span><span class="p">,</span> <span class="mi">1</span><span class="o">-</span><span class="n">p_head</span><span class="p">],</span> <span class="n">k</span><span class="o">=</span><span class="n">num_tosses</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">num_tosses</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="nb">input</span><span class="p">(</span><span class="s">"Number of coin tosses: "</span><span class="p">))</span>
    <span class="n">p_head</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="nb">input</span><span class="p">(</span><span class="s">"Probability of tossing heads (between 0 and 1, default=0.5): "</span><span class="p">)</span> <span class="ow">or</span> <span class="s">"0.5"</span><span class="p">)</span>
    <span class="n">tosses</span> <span class="o">=</span> <span class="n">coin_toss</span><span class="p">(</span><span class="n">num_tosses</span><span class="p">,</span> <span class="n">p_head</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Results after </span><span class="si">{</span><span class="n">num_tosses</span><span class="si">}</span><span class="s"> tosses:"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">", "</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">tosses</span><span class="p">))</span>
    <span class="n">heads_count</span> <span class="o">=</span> <span class="n">tosses</span><span class="p">.</span><span class="n">count</span><span class="p">(</span><span class="s">"H"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Heads: </span><span class="si">{</span><span class="n">heads_count</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="n">tails_count</span> <span class="o">=</span> <span class="n">tosses</span><span class="p">.</span><span class="n">count</span><span class="p">(</span><span class="s">"T"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Tails: </span><span class="si">{</span><span class="n">tails_count</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>The method <code class="language-plaintext highlighter-rouge">coin_toss()</code> generates our facts which we record in the variable <code class="language-plaintext highlighter-rouge">tosses</code>. My output from tossing 10 times using the default probability of 0.5 is</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Number of coin tosses: 10
Probability of tossing heads (between 0 and 1, default=0.5): 
Results after 10 tosses:
H, H, T, T, H, T, T, T, H, T
Heads: 4
Tails: 6
</code></pre></div></div>

<p>Now that we have a model of our coin toss world, we can create different outcomes every time we run the code example. We can pick probabilities that are different from a fair coin (p=0.5). We can repeat the coin toss but for more tosses. Here’s a thousand, with a probability of heads of 0.2.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Number of coin tosses: 1000
Probability of tossing heads (between 0 and 1, default=0.5): 0.2
Results after 1000 tosses:
H, H, T, T, T, T, T, T, H, T, H, T, T, H, T, T, H, T, T, T, T, T, T, H, T, T, H, T, T, H, H, T, T, T, T, T, T, H, H, T, T, T, T, T, T, T, T, T, T, T, H, T, T, H, T, T, T, T, T, T, H, T, T, T, T, T, T, T, T, T, T, T, H, T, T, H, T, T, T, T, T, T, T, T, T, H, T, T, H, H, T, T, T, T, T, T, H, T, H, T, H, H, T, H, T, T, T, T, T, T, T, T, T, H, T, T, T, H, T, T, H, T, T, T, T, H, H, T, T, T, T, T, T, T, T, H, T, H, T, T, T, T, T, H, T, T, H, T, H, T, H, T, T, T, T, T, H, T, T, H, T, H, T, T, T, T, H, T, T, H, T, T, T, T, T, H, T, T, T, T, H, T, T, T, T, T, T, H, T, T, T, T, H, T, T, T, T, H, T, T, H, T, T, T, T, T, H, H, T, T, H, T, T, T, T, H, T, T, H, T, T, T, T, T, T, T, T, T, T, T, H, T, T, T, T, T, T, T, H, H, T, H, H, T, T, T, T, T, T, T, T, H, T, T, T, T, T, T, T, T, T, T, T, T, T, H, T, T, T, T, T, T, T, T, T, T, H, H, T, T, T, T, T, T, T, T, T, T, T, H, T, T, T, H, H, T, T, H, T, T, T, T, T, T, T, H, T, T, T, T, H, T, T, T, T, H, T, T, T, T, T, H, T, T, T, T, T, T, H, T, T, T, T, T, T, T, T, T, T, T, H, T, T, H, T, T, T, T, T, T, H, T, T, H, T, T, T, T, T, T, T, T, H, T, H, T, T, T, T, H, T, T, T, T, T, T, T, T, H, T, T, T, T, T, H, H, T, T, H, T, H, T, H, T, T, T, T, H, H, H, T, T, T, H, T, T, T, H, T, T, T, T, T, T, H, T, T, H, T, T, H, T, T, T, T, T, H, T, H, T, T, H, T, T, T, T, T, T, T, H, H, T, T, T, T, T, T, T, T, T, T, T, T, T, H, H, T, T, T, T, T, T, H, T, H, H, T, T, T, T, T, H, T, H, H, T, H, T, T, T, T, T, T, T, T, H, T, T, T, H, H, T, T, T, T, T, H, T, T, T, T, T, T, T, T, T, H, H, T, T, T, H, H, T, T, T, T, T, T, H, H, T, H, H, T, H, T, H, T, H, T, H, T, H, T, T, T, H, T, T, T, T, T, H, T, H, T, T, T, T, T, H, H, H, T, T, H, T, T, T, H, T, T, T, T, T, T, T, T, T, H, H, H, H, H, H, H, T, T, T, T, H, T, T, H, T, T, T, T, T, H, T, T, T, H, T, H, T, T, T, T, H, T, T, T, T, T, T, T, T, T, T, T, T, H, T, T, T, T, T, T, T, T, T, T, T, T, T, T, H, H, T, T, H, T, T, T, T, T, H, T, T, T, T, T, T, T, H, T, T, H, T, T, T, H, H, T, T, T, T, T, T, T, T, T, H, T, T, H, T, T, T, H, T, T, H, T, T, T, T, T, T, T, T, H, T, T, H, T, H, T, H, T, H, T, T, T, H, T, T, T, H, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, H, T, T, T, T, T, T, T, T, T, T, T, H, T, T, T, T, T, T, T, T, H, T, T, T, T, T, T, T, T, H, H, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, H, T, T, T, T, T, H, T, T, T, T, T, T, H, T, H, T, H, T, T, H, H, T, T, T, T, H, T, T, T, T, T, T, T, T, H, T, H, H, H, T, T, T, T, H, T, T, H, T, T, H, T, T, H, T, T, T, T, H, T, T, H, T, T, H, T, T, H, T, H, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, H, T, T, T, T, T, T, T, H, T, H, T, T, T, H, T, H, H, T, T, T, T, T, H, T, T, T, T, T, T, T, H, H, T, T, T, T, T, H, T, T, T, T, T, T, H, T, T, T, T, T, T, T, T, T, T, H, H, T, T, T, T, T, H, T, T, T, H, H, T, H, T, T, T, H, T, T, T, T, H, T, H, T, T, T, T, T, T, T, T, T, H, T, H, T, T, T, T, T, T, T, T, T, T, T, T, H, H, T, T, T, T, T, T, T, T, T, T, T, H, T, T, T, T, H, T, T, T, T, H, T, T, T, T, H, H, T, T, T, T, T, T, T
Heads: 218
Tails: 782
</code></pre></div></div>

<p>We can see that we don’t always get the exact share of heads and tails (200 H out of 1000 tosses) and that’s ok. Tossing a coin, as we know from the real world creates different outcomes every time we do it. That’s why in football</p>

<blockquote>
  <p>the referee tosses a coin and the team that wins the toss decides which goal to attack in the first half or to take the kick-off. - <a href="https://www.thefa.com/football-rules-governance/lawsandrules/laws/football-11-11/law-8---the-start-and-restart-of-play">Law 8: The Start and Restart of Play</a></p>
</blockquote>

<p>That’s why our model contains randomness in the first place (<code class="language-plaintext highlighter-rouge">random.choices(["H", "T"],...)</code>. We know that coin tossing looks random, so our model must reflect the fact.</p>

<h1 id="fact-number-three-examining-your-model-lets-you-figure-out-the-why">Fact number three: Examining your model lets you figure out the <em>Why</em>.</h1>

<p>That’s it. The <em>data generating process</em>.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>In his book <a href="https://simonsinek.com/books/start-with-why/">Start with WHY</a>, Simon Sinek lines up the “What”, the “How”, and the “Why” in what he calls <em>The Golden Circle</em>. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>To sound more professional, you should call it <em>synthetic data</em>. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Mario</name></author><summary type="html"><![CDATA[Hello. If you, like me, are looking at data all day every day (or just sometimes), you want to start thinking about The Data Generating Process. According to one of the shortest articles I’ve seen on Wikipedia a data generating process is a process in the real world that “generates” the data one is interested in.]]></summary></entry><entry><title type="html">Environmental Data Science Jupyter book</title><link href="/2023/07/01/eds-jupyter-book.html" rel="alternate" type="text/html" title="Environmental Data Science Jupyter book" /><published>2023-07-01T00:00:00+00:00</published><updated>2023-07-01T00:00:00+00:00</updated><id>/2023/07/01/eds-jupyter-book</id><content type="html" xml:base="/2023/07/01/eds-jupyter-book.html"><![CDATA[<p>Recently, I have been thinking more and more about collecting all the bits and bobs,the tools and the snippets of code I have assembled over the years, and to put it all into a single document.
I have seen other people putting their lecture material online in the form of a <a href="https://jupyterbook.org/en/stable/intro.html">jupyter book</a>, for example <a href="https://earth-env-data-science.github.io/intro.html">Ryan Abernathy’s Earth and Environmental Data Science</a> or <a href="https://brian-rose.github.io/ClimateLaboratoryBook/home.html">Brian Rose’s Climate Laboratory</a>.</p>

<p>What I like about jupyter books, in particular, is that you can integrate your code snippets and the output will be automatically rendered as HTML content. A nice little side effect of it is that you will know whether your code works or not. A little bit like unit testing in software development.</p>

<p>jupyter {book} feature list:</p>
<ul>
  <li><a href="https://jupyterbook.org/en/stable/file-types/index.html#allowed-content-types">Support</a> for <a href="https://jupyterbook.org/en/stable/file-types/markdown.html">Markdown files</a>, <a href="https://jupyterbook.org/en/stable/file-types/notebooks.html">Jupyer notebooks</a>, and <a href="https://jupyterbook.org/en/stable/file-types/myst-notebooks.html">MyST Markdown notebooks</a> (Markdown files that will be converted to a notebook and excecuted 🤩)</li>
  <li><a href="https://jupyterbook.org/en/stable/start/build.html#preview-your-built-html">Preview your content</a></li>
  <li><a href="https://jupyterbook.org/en/stable/advanced/pdf.html">Build a PDF</a></li>
  <li><a href="https://jupyterbook.org/en/stable/content/citations.html">Citations and bibliographies</a></li>
  <li><a href="https://jupyterbook.org/en/stable/content/math.html">Math and equation support</a></li>
  <li><a href="https://jupyterbook.org/en/stable/interactive/interactive.html">Interactive data visualizations</a></li>
</ul>

<p>What I haven’t figured out, yet, is how to integrate the HTML content into <a href="https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll">Jekyll, a framework that GitHub Pages uses</a> for turning Markdown files into a website (like this one). I would like to have that content on a subdomain.</p>

<p>Wait and see.</p>]]></content><author><name>Mario</name></author><summary type="html"><![CDATA[Recently, I have been thinking more and more about collecting all the bits and bobs,the tools and the snippets of code I have assembled over the years, and to put it all into a single document. I have seen other people putting their lecture material online in the form of a jupyter book, for example Ryan Abernathy’s Earth and Environmental Data Science or Brian Rose’s Climate Laboratory.]]></summary></entry><entry><title type="html">Noise models for (paleo)climate applications</title><link href="/2022/09/08/noise_models.html" rel="alternate" type="text/html" title="Noise models for (paleo)climate applications" /><published>2022-09-08T00:00:00+00:00</published><updated>2022-09-08T00:00:00+00:00</updated><id>/2022/09/08/noise_models</id><content type="html" xml:base="/2022/09/08/noise_models.html"><![CDATA[<p>Insipred by <a href="https://blog.ioces.com/matt/posts/colouring-noise/">Matt Schubert</a>, I wanted to recreate his coloured noise models. Check out my Github repo <a href="https://github.com/mkrapp/noise4paleo">Noise4Paleo</a>.</p>

<p><strong>Pink noise</strong>:</p>

<p><img src="/assets/pink_noise.png" alt="" /></p>

<p><strong>Reproducing Figures 1 and 2 from <em>Vaughan<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> et al. (2011)</em></strong>:</p>

<p><img src="/assets/vaughan_fig1_2.png" alt="" /></p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Vaughan, S., R. J. Bailey, and D. G. Smith. <em>Detecting Cycles in Stratigraphic Data: Spectral Analysis in the Presence of Red Noise</em>. Paleoceanography 26 (4), 2011. <a href="https://doi.org/10.1029/2011PA002195">link</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Mario</name></author><summary type="html"><![CDATA[Insipred by Matt Schubert, I wanted to recreate his coloured noise models. Check out my Github repo Noise4Paleo.]]></summary></entry><entry><title type="html">Wavelet analysis</title><link href="/2022/03/31/wavelet-analysis.html" rel="alternate" type="text/html" title="Wavelet analysis" /><published>2022-03-31T00:00:00+00:00</published><updated>2022-03-31T00:00:00+00:00</updated><id>/2022/03/31/wavelet-analysis</id><content type="html" xml:base="/2022/03/31/wavelet-analysis.html"><![CDATA[<p>The interactive wavelet tool by Torrence &amp; Compo from their “A Practical Guide to Wavelet Analysis” website was my go-to thing for running a wavelet analysis on my time series data for years.
It has been offline for quite some time now.
Anyway, <a href="https://github.com/mkrapp/wavelet-analysis">here</a> it is again, ready for your wavelet analysis:</p>

<p>Use <code class="language-plaintext highlighter-rouge">docker</code> to deploy and run locally:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose <span class="nt">-f</span> docker-compose.yml up <span class="nt">--build</span>
</code></pre></div></div>

<p>It used to run on Heroku’s free plan, which they have removed since then ¯\<em>(ツ)</em>/¯.</p>

<p><strong>Update</strong>:</p>

<p>It’s up and running again on <a href="https://wavelet-analysis.onrender.com/">render.com</a></p>]]></content><author><name>Mario</name></author><summary type="html"><![CDATA[The interactive wavelet tool by Torrence &amp; Compo from their “A Practical Guide to Wavelet Analysis” website was my go-to thing for running a wavelet analysis on my time series data for years. It has been offline for quite some time now. Anyway, here it is again, ready for your wavelet analysis:]]></summary></entry></feed>