<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>tishad</title>
  <subtitle>How a process gets from bench to plant, and what chemical engineering practice becomes when data, models and assistants are ordinary tools.</subtitle>
  <link href="https://tishad.net/" />
  <link rel="self" href="https://tishad.net/atom.xml" />
  <id>https://tishad.net/</id>
  <updated>2026-10-13T00:00:00.000Z</updated>
  <author><name>Md Imranul Haque</name></author>
  <rights>CC BY 4.0</rights>
  <entry>
    <title>Where I stand with AI, and where a reader might</title>
    <link href="https://tishad.net/essays/where-i-stand-with-ai/" />
    <id>https://tishad.net/essays/where-i-stand-with-ai/</id>
    <published>2026-10-11T00:00:00.000Z</published>
    <updated>2026-10-13T00:00:00.000Z</updated>
    <summary>One year from doubting that AI assistants were any use to me and worrying about what they cost in energy, to using one every working day, told in order from my own record, with the doubts that stayed.</summary>
    <content type="html">&lt;p&gt;A year ago I did not believe AI assistants were useful for my kind of work, and I thought what they cost in energy was hard to defend. Now I use one every working day. I did not get there overnight, and I did not change my mind about everything, so I want to tell it in the order it happened.&lt;/p&gt;
&lt;p&gt;&lt;svg class=&quot;plot learning-year figure-svg&quot; role=&quot;img&quot; aria-label=&quot;One year by month: days with an assistant session, messages I wrote, and the tools and methods I met for the first time&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; width=&quot;518.740157pt&quot; height=&quot;187.086614pt&quot; viewBox=&quot;0 0 518.740157 187.086614&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; version=&quot;1.1&quot;&gt;
 &lt;defs&gt;
  &lt;style type=&quot;text/css&quot;&gt;*{stroke-linejoin: round; stroke-linecap: butt}&lt;/style&gt;
 &lt;/defs&gt;
 &lt;g id=&quot;figure_1&quot;&gt;
  &lt;g id=&quot;patch_1&quot;&gt;
   &lt;path d=&quot;M 0 187.086614 
L 518.740157 187.086614 
L 518.740157 0 
L 0 0 
L 0 187.086614 
z
&quot; style=&quot;fill: none&quot;&gt;&lt;/path&gt;
  &lt;/g&gt;
  &lt;g id=&quot;axes_1&quot;&gt;
   &lt;g id=&quot;patch_2&quot;&gt;
    &lt;path d=&quot;M 25.937008 151.540157 
L 129.685039 151.540157 
L 129.685039 28.062992 
L 25.937008 28.062992 
L 25.937008 151.540157 
z
&quot; style=&quot;fill: none&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_3&quot;&gt;
    &lt;path d=&quot;M 30.652827 151.540157 
L 36.295689 151.540157 
L 36.295689 70.126642 
L 30.652827 70.126642 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_4&quot;&gt;
    &lt;path d=&quot;M 38.714058 151.540157 
L 44.356919 151.540157 
L 44.356919 119.879346 
L 38.714058 119.879346 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_5&quot;&gt;
    &lt;path d=&quot;M 46.775288 151.540157 
L 52.418149 151.540157 
L 52.418149 83.695561 
L 46.775288 83.695561 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_6&quot;&gt;
    &lt;path d=&quot;M 54.836518 151.540157 
L 60.479379 151.540157 
L 60.479379 33.942857 
L 54.836518 33.942857 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_7&quot;&gt;
    &lt;path d=&quot;M 62.897748 151.540157 
L 68.540609 151.540157 
L 68.540609 61.080696 
L 62.897748 61.080696 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_8&quot;&gt;
    &lt;path d=&quot;M 70.958978 151.540157 
L 76.601839 151.540157 
L 76.601839 83.695561 
L 70.958978 83.695561 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_9&quot;&gt;
    &lt;path d=&quot;M 79.020208 151.540157 
L 84.663069 151.540157 
L 84.663069 92.741507 
L 79.020208 92.741507 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_10&quot;&gt;
    &lt;path d=&quot;M 87.081438 151.540157 
L 92.724299 151.540157 
L 92.724299 133.448265 
L 87.081438 133.448265 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_11&quot;&gt;
    &lt;path d=&quot;M 95.142668 151.540157 
L 100.785529 151.540157 
L 100.785529 65.603669 
L 95.142668 65.603669 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_12&quot;&gt;
    &lt;path d=&quot;M 103.203898 151.540157 
L 108.84676 151.540157 
L 108.84676 65.603669 
L 103.203898 65.603669 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_13&quot;&gt;
    &lt;path d=&quot;M 111.265129 151.540157 
L 116.90799 151.540157 
L 116.90799 33.942857 
L 111.265129 33.942857 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_14&quot;&gt;
    &lt;path d=&quot;M 119.326359 151.540157 
L 124.96922 151.540157 
L 124.96922 101.787453 
L 119.326359 101.787453 
L 119.326359 151.540157 
z
&quot; clip-path=&quot;url(#p77b829a51f)&quot; style=&quot;fill: none; stroke: var(--accent,currentColor); stroke-width: 0.6; stroke-linejoin: miter&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;matplotlib.axis_1&quot;&gt;
    &lt;g id=&quot;xtick_1&quot;&gt;
     &lt;g id=&quot;line2d_1&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_1&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;33.474258&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 33.474258 159.334845)&quot;&gt;N&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_2&quot;&gt;
     &lt;g id=&quot;line2d_2&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_2&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;41.535488&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 41.535488 159.334845)&quot;&gt;D&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_3&quot;&gt;
     &lt;g id=&quot;line2d_3&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_3&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;49.596718&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 49.596718 159.334845)&quot;&gt;J&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_4&quot;&gt;
     &lt;g id=&quot;line2d_4&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_4&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;57.657948&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 57.657948 159.334845)&quot;&gt;F&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_5&quot;&gt;
     &lt;g id=&quot;line2d_5&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_5&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;65.719178&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 65.719178 159.334845)&quot;&gt;M&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_6&quot;&gt;
     &lt;g id=&quot;line2d_6&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_6&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;73.780409&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 73.780409 159.334845)&quot;&gt;A&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_7&quot;&gt;
     &lt;g id=&quot;line2d_7&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_7&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;81.841639&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 81.841639 159.334845)&quot;&gt;M&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_8&quot;&gt;
     &lt;g id=&quot;line2d_8&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_8&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;89.902869&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 89.902869 159.334845)&quot;&gt;J&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_9&quot;&gt;
     &lt;g id=&quot;line2d_9&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_9&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;97.964099&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 97.964099 159.334845)&quot;&gt;J&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_10&quot;&gt;
     &lt;g id=&quot;line2d_10&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_10&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;106.025329&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 106.025329 159.334845)&quot;&gt;A&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_11&quot;&gt;
     &lt;g id=&quot;line2d_11&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_11&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;114.086559&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 114.086559 159.334845)&quot;&gt;S&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_12&quot;&gt;
     &lt;g id=&quot;line2d_12&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_12&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;122.147789&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 122.147789 159.334845)&quot;&gt;O&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
   &lt;/g&gt;
   &lt;g id=&quot;matplotlib.axis_2&quot;&gt;
    &lt;g id=&quot;ytick_1&quot;&gt;
     &lt;g id=&quot;line2d_13&quot;&gt;
      &lt;defs&gt;
       &lt;path id=&quot;m0c35056a2f&quot; d=&quot;M 0 0 
L -2.5 0 
&quot; style=&quot;stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/path&gt;
      &lt;/defs&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;25.937008&quot; y=&quot;151.540157&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_13&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;19.937008&quot; y=&quot;153.687501&quot; transform=&quot;rotate(-0 19.937008 153.687501)&quot;&gt;0&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_2&quot;&gt;
     &lt;g id=&quot;line2d_14&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;25.937008&quot; y=&quot;128.925292&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_14&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;19.937008&quot; y=&quot;131.072636&quot; transform=&quot;rotate(-0 19.937008 131.072636)&quot;&gt;5&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_3&quot;&gt;
     &lt;g id=&quot;line2d_15&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;25.937008&quot; y=&quot;106.310427&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_15&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;19.937008&quot; y=&quot;108.45777&quot; transform=&quot;rotate(-0 19.937008 108.45777)&quot;&gt;10&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_4&quot;&gt;
     &lt;g id=&quot;line2d_16&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;25.937008&quot; y=&quot;83.695561&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_16&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;19.937008&quot; y=&quot;85.842905&quot; transform=&quot;rotate(-0 19.937008 85.842905)&quot;&gt;15&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_5&quot;&gt;
     &lt;g id=&quot;line2d_17&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;25.937008&quot; y=&quot;61.080696&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_17&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;19.937008&quot; y=&quot;63.228039&quot; transform=&quot;rotate(-0 19.937008 63.228039)&quot;&gt;20&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_6&quot;&gt;
     &lt;g id=&quot;line2d_18&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;25.937008&quot; y=&quot;38.46583&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_18&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;19.937008&quot; y=&quot;40.613174&quot; transform=&quot;rotate(-0 19.937008 40.613174)&quot;&gt;25&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;text_19&quot;&gt;
     &lt;text style=&quot;font-size: 7px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;7.790602&quot; y=&quot;89.801575&quot; transform=&quot;rotate(-90 7.790602 89.801575)&quot;&gt;days with an assistant session&lt;/text&gt;
    &lt;/g&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_15&quot;&gt;
    &lt;path d=&quot;M 25.937008 151.540157 
L 25.937008 28.062992 
&quot; style=&quot;fill: none; stroke: var(--rule,currentColor); stroke-width: 0.5; stroke-linejoin: miter; stroke-linecap: square&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_16&quot;&gt;
    &lt;path d=&quot;M 25.937008 151.540157 
L 129.685039 151.540157 
&quot; style=&quot;fill: none; stroke: var(--rule,currentColor); stroke-width: 0.5; stroke-linejoin: miter; stroke-linecap: square&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_20&quot;&gt;
    &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: start; fill: currentColor; opacity: 0.75&quot; x=&quot;25.937008&quot; y=&quot;175.000819&quot; transform=&quot;rotate(-0 25.937008 175.000819)&quot;&gt;2025&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_21&quot;&gt;
    &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: start; fill: currentColor; opacity: 0.75&quot; x=&quot;44.95748&quot; y=&quot;175.000819&quot; transform=&quot;rotate(-0 44.95748 175.000819)&quot;&gt;2026&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_22&quot;&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor; opacity: 0.75&quot; x=&quot;129.685039&quot; y=&quot;25.593449&quot; transform=&quot;rotate(-0 129.685039 25.593449)&quot;&gt;open bar: month in progress&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_23&quot;&gt;
    &lt;text style=&quot;font-weight: 700; font-size: 8px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;11.412283&quot; y=&quot;21.545156&quot; transform=&quot;rotate(-0 11.412283 21.545156)&quot;&gt;a&lt;/text&gt;
   &lt;/g&gt;
  &lt;/g&gt;
  &lt;g id=&quot;axes_2&quot;&gt;
   &lt;g id=&quot;patch_17&quot;&gt;
    &lt;path d=&quot;M 165.99685 151.540157 
L 269.744882 151.540157 
L 269.744882 28.062992 
L 165.99685 28.062992 
L 165.99685 151.540157 
z
&quot; style=&quot;fill: none&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_18&quot;&gt;
    &lt;path d=&quot;M 170.71267 151.540157 
L 176.355531 151.540157 
L 176.355531 132.78785 
L 170.71267 132.78785 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_19&quot;&gt;
    &lt;path d=&quot;M 178.7739 151.540157 
L 184.416761 151.540157 
L 184.416761 148.490083 
L 178.7739 148.490083 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_20&quot;&gt;
    &lt;path d=&quot;M 186.83513 151.540157 
L 192.477991 151.540157 
L 192.477991 124.202457 
L 186.83513 124.202457 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_21&quot;&gt;
    &lt;path d=&quot;M 194.89636 151.540157 
L 200.539221 151.540157 
L 200.539221 33.942857 
L 194.89636 33.942857 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_22&quot;&gt;
    &lt;path d=&quot;M 202.95759 151.540157 
L 208.600452 151.540157 
L 208.600452 118.32824 
L 202.95759 118.32824 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_23&quot;&gt;
    &lt;path d=&quot;M 211.018821 151.540157 
L 216.661682 151.540157 
L 216.661682 140.130621 
L 211.018821 140.130621 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_24&quot;&gt;
    &lt;path d=&quot;M 219.080051 151.540157 
L 224.722912 151.540157 
L 224.722912 146.117804 
L 219.080051 146.117804 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_25&quot;&gt;
    &lt;path d=&quot;M 227.141281 151.540157 
L 232.784142 151.540157 
L 232.784142 148.828981 
L 227.141281 148.828981 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_26&quot;&gt;
    &lt;path d=&quot;M 235.202511 151.540157 
L 240.845372 151.540157 
L 240.845372 135.047164 
L 235.202511 135.047164 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_27&quot;&gt;
    &lt;path d=&quot;M 243.263741 151.540157 
L 248.906602 151.540157 
L 248.906602 133.465644 
L 243.263741 133.465644 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_28&quot;&gt;
    &lt;path d=&quot;M 251.324971 151.540157 
L 256.967832 151.540157 
L 256.967832 128.043291 
L 251.324971 128.043291 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_29&quot;&gt;
    &lt;path d=&quot;M 259.386201 151.540157 
L 265.029062 151.540157 
L 265.029062 106.805738 
L 259.386201 106.805738 
L 259.386201 151.540157 
z
&quot; clip-path=&quot;url(#pff2633bbba)&quot; style=&quot;fill: none; stroke: var(--accent,currentColor); stroke-width: 0.6; stroke-linejoin: miter&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;matplotlib.axis_3&quot;&gt;
    &lt;g id=&quot;xtick_13&quot;&gt;
     &lt;g id=&quot;line2d_19&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_24&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;173.534101&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 173.534101 159.334845)&quot;&gt;N&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_14&quot;&gt;
     &lt;g id=&quot;line2d_20&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_25&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;181.595331&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 181.595331 159.334845)&quot;&gt;D&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_15&quot;&gt;
     &lt;g id=&quot;line2d_21&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_26&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;189.656561&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 189.656561 159.334845)&quot;&gt;J&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_16&quot;&gt;
     &lt;g id=&quot;line2d_22&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_27&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;197.717791&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 197.717791 159.334845)&quot;&gt;F&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_17&quot;&gt;
     &lt;g id=&quot;line2d_23&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_28&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;205.779021&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 205.779021 159.334845)&quot;&gt;M&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_18&quot;&gt;
     &lt;g id=&quot;line2d_24&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_29&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;213.840251&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 213.840251 159.334845)&quot;&gt;A&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_19&quot;&gt;
     &lt;g id=&quot;line2d_25&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_30&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;221.901481&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 221.901481 159.334845)&quot;&gt;M&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_20&quot;&gt;
     &lt;g id=&quot;line2d_26&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_31&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;229.962711&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 229.962711 159.334845)&quot;&gt;J&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_21&quot;&gt;
     &lt;g id=&quot;line2d_27&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_32&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;238.023941&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 238.023941 159.334845)&quot;&gt;J&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_22&quot;&gt;
     &lt;g id=&quot;line2d_28&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_33&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;246.085172&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 246.085172 159.334845)&quot;&gt;A&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_23&quot;&gt;
     &lt;g id=&quot;line2d_29&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_34&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;254.146402&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 254.146402 159.334845)&quot;&gt;S&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_24&quot;&gt;
     &lt;g id=&quot;line2d_30&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_35&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;262.207632&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 262.207632 159.334845)&quot;&gt;O&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
   &lt;/g&gt;
   &lt;g id=&quot;matplotlib.axis_4&quot;&gt;
    &lt;g id=&quot;ytick_7&quot;&gt;
     &lt;g id=&quot;line2d_31&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;165.99685&quot; y=&quot;151.540157&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_36&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;159.99685&quot; y=&quot;153.687501&quot; transform=&quot;rotate(-0 159.99685 153.687501)&quot;&gt;0&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_8&quot;&gt;
     &lt;g id=&quot;line2d_32&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;165.99685&quot; y=&quot;128.947016&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_37&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;159.99685&quot; y=&quot;131.09436&quot; transform=&quot;rotate(-0 159.99685 131.09436)&quot;&gt;200&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_9&quot;&gt;
     &lt;g id=&quot;line2d_33&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;165.99685&quot; y=&quot;106.353875&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_38&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;159.99685&quot; y=&quot;108.501219&quot; transform=&quot;rotate(-0 159.99685 108.501219)&quot;&gt;400&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_10&quot;&gt;
     &lt;g id=&quot;line2d_34&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;165.99685&quot; y=&quot;83.760734&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_39&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;159.99685&quot; y=&quot;85.908077&quot; transform=&quot;rotate(-0 159.99685 85.908077)&quot;&gt;600&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_11&quot;&gt;
     &lt;g id=&quot;line2d_35&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;165.99685&quot; y=&quot;61.167592&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_40&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;159.99685&quot; y=&quot;63.314936&quot; transform=&quot;rotate(-0 159.99685 63.314936)&quot;&gt;800&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_12&quot;&gt;
     &lt;g id=&quot;line2d_36&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;165.99685&quot; y=&quot;38.574451&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_41&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;159.99685&quot; y=&quot;40.721795&quot; transform=&quot;rotate(-0 159.99685 40.721795)&quot;&gt;1000&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;text_42&quot;&gt;
     &lt;text style=&quot;font-size: 7px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;141.177319&quot; y=&quot;89.801575&quot; transform=&quot;rotate(-90 141.177319 89.801575)&quot;&gt;messages I wrote to an assistant&lt;/text&gt;
    &lt;/g&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_30&quot;&gt;
    &lt;path d=&quot;M 165.99685 151.540157 
L 165.99685 28.062992 
&quot; style=&quot;fill: none; stroke: var(--rule,currentColor); stroke-width: 0.5; stroke-linejoin: miter; stroke-linecap: square&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_31&quot;&gt;
    &lt;path d=&quot;M 165.99685 151.540157 
L 269.744882 151.540157 
&quot; style=&quot;fill: none; stroke: var(--rule,currentColor); stroke-width: 0.5; stroke-linejoin: miter; stroke-linecap: square&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_43&quot;&gt;
    &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: start; fill: currentColor; opacity: 0.75&quot; x=&quot;165.99685&quot; y=&quot;175.000819&quot; transform=&quot;rotate(-0 165.99685 175.000819)&quot;&gt;2025&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_44&quot;&gt;
    &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: start; fill: currentColor; opacity: 0.75&quot; x=&quot;185.017323&quot; y=&quot;175.000819&quot; transform=&quot;rotate(-0 185.017323 175.000819)&quot;&gt;2026&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_45&quot;&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor; opacity: 0.75&quot; x=&quot;269.744882&quot; y=&quot;25.593449&quot; transform=&quot;rotate(-0 269.744882 25.593449)&quot;&gt;open bar: month in progress&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_46&quot;&gt;
    &lt;text style=&quot;font-weight: 700; font-size: 8px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;151.472126&quot; y=&quot;21.545156&quot; transform=&quot;rotate(-0 151.472126 21.545156)&quot;&gt;b&lt;/text&gt;
   &lt;/g&gt;
  &lt;/g&gt;
  &lt;g id=&quot;axes_3&quot;&gt;
   &lt;g id=&quot;patch_32&quot;&gt;
    &lt;path d=&quot;M 306.056693 151.540157 
L 508.365354 151.540157 
L 508.365354 28.062992 
L 306.056693 28.062992 
L 306.056693 151.540157 
z
&quot; style=&quot;fill: none&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;matplotlib.axis_5&quot;&gt;
    &lt;g id=&quot;xtick_25&quot;&gt;
     &lt;g id=&quot;line2d_37&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_47&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;314.629094&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 314.629094 159.334845)&quot;&gt;N&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_26&quot;&gt;
     &lt;g id=&quot;line2d_38&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_48&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;331.773896&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 331.773896 159.334845)&quot;&gt;D&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_27&quot;&gt;
     &lt;g id=&quot;line2d_39&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_49&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;348.918697&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 348.918697 159.334845)&quot;&gt;J&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_28&quot;&gt;
     &lt;g id=&quot;line2d_40&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_50&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;366.063499&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 366.063499 159.334845)&quot;&gt;F&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_29&quot;&gt;
     &lt;g id=&quot;line2d_41&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_51&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;383.208301&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 383.208301 159.334845)&quot;&gt;M&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_30&quot;&gt;
     &lt;g id=&quot;line2d_42&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_52&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;400.353103&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 400.353103 159.334845)&quot;&gt;A&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_31&quot;&gt;
     &lt;g id=&quot;line2d_43&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_53&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;417.497905&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 417.497905 159.334845)&quot;&gt;M&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_32&quot;&gt;
     &lt;g id=&quot;line2d_44&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_54&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;434.642707&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 434.642707 159.334845)&quot;&gt;J&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_33&quot;&gt;
     &lt;g id=&quot;line2d_45&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_55&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;451.787508&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 451.787508 159.334845)&quot;&gt;J&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_34&quot;&gt;
     &lt;g id=&quot;line2d_46&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_56&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;468.93231&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 468.93231 159.334845)&quot;&gt;A&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_35&quot;&gt;
     &lt;g id=&quot;line2d_47&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_57&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;486.077112&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 486.077112 159.334845)&quot;&gt;S&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_36&quot;&gt;
     &lt;g id=&quot;line2d_48&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_58&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;503.221914&quot; y=&quot;159.334845&quot; transform=&quot;rotate(-0 503.221914 159.334845)&quot;&gt;O&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
   &lt;/g&gt;
   &lt;g id=&quot;matplotlib.axis_6&quot;&gt;
    &lt;g id=&quot;ytick_13&quot;&gt;
     &lt;g id=&quot;line2d_49&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;306.056693&quot; y=&quot;151.540157&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_59&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;300.056693&quot; y=&quot;153.687501&quot; transform=&quot;rotate(-0 300.056693 153.687501)&quot;&gt;0&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_14&quot;&gt;
     &lt;g id=&quot;line2d_50&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;306.056693&quot; y=&quot;129.171106&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_60&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;300.056693&quot; y=&quot;131.31845&quot; transform=&quot;rotate(-0 300.056693 131.31845)&quot;&gt;10&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_15&quot;&gt;
     &lt;g id=&quot;line2d_51&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;306.056693&quot; y=&quot;106.802054&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_61&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;300.056693&quot; y=&quot;108.949398&quot; transform=&quot;rotate(-0 300.056693 108.949398)&quot;&gt;20&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_16&quot;&gt;
     &lt;g id=&quot;line2d_52&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;306.056693&quot; y=&quot;84.433002&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_62&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;300.056693&quot; y=&quot;86.580346&quot; transform=&quot;rotate(-0 300.056693 86.580346)&quot;&gt;30&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_17&quot;&gt;
     &lt;g id=&quot;line2d_53&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;306.056693&quot; y=&quot;62.063951&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_63&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;300.056693&quot; y=&quot;64.211294&quot; transform=&quot;rotate(-0 300.056693 64.211294)&quot;&gt;40&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_18&quot;&gt;
     &lt;g id=&quot;line2d_54&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m0c35056a2f&quot; x=&quot;306.056693&quot; y=&quot;39.694899&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_64&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;300.056693&quot; y=&quot;41.842243&quot; transform=&quot;rotate(-0 300.056693 41.842243)&quot;&gt;50&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;text_65&quot;&gt;
     &lt;text style=&quot;font-size: 7px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;287.992318&quot; y=&quot;89.801575&quot; transform=&quot;rotate(-90 287.992318 89.801575)&quot;&gt;tools, methods and habits met&lt;/text&gt;
    &lt;/g&gt;
   &lt;/g&gt;
   &lt;g id=&quot;line2d_55&quot;&gt;
    &lt;path d=&quot;M 306.056693 151.540157 
L 308.872856 151.540157 
L 308.872856 147.066347 
L 310.562554 147.066347 
L 310.562554 142.592537 
L 312.252252 142.592537 
L 312.252252 140.355632 
L 313.94195 140.355632 
L 313.94195 138.118726 
L 314.505183 138.118726 
L 314.505183 133.644916 
L 316.758113 133.644916 
L 316.758113 131.408011 
L 319.574276 131.408011 
L 319.574276 120.223485 
L 348.862374 120.223485 
L 348.862374 106.802054 
L 358.437329 106.802054 
L 358.437329 104.565149 
L 360.127027 104.565149 
L 360.127027 102.328244 
L 360.69026 102.328244 
L 360.69026 97.854433 
L 361.253492 97.854433 
L 361.253492 91.143718 
L 361.816725 91.143718 
L 361.816725 84.433002 
L 362.94319 84.433002 
L 362.94319 82.196097 
L 363.506423 82.196097 
L 363.506423 79.959192 
L 364.069656 79.959192 
L 364.069656 77.722287 
L 365.196121 77.722287 
L 365.196121 75.485382 
L 366.885819 75.485382 
L 366.885819 73.248477 
L 369.701982 73.248477 
L 369.701982 71.011571 
L 476.152953 71.011571 
L 476.152953 68.774666 
L 482.911744 68.774666 
L 482.911744 64.300856 
L 493.049932 64.300856 
L 493.049932 62.063951 
L 494.176397 62.063951 
L 494.176397 59.827046 
L 495.302863 59.827046 
L 495.302863 57.59014 
L 496.992561 57.59014 
L 496.992561 55.353235 
L 497.555793 55.353235 
L 497.555793 53.11633 
L 499.808724 53.11633 
L 499.808724 48.64252 
L 500.371957 48.64252 
L 500.371957 48.64252 
&quot; clip-path=&quot;url(#p037e41cebd)&quot; style=&quot;fill: none; stroke: currentColor; stroke-linecap: square&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;line2d_56&quot;&gt;
    &lt;defs&gt;
     &lt;path id=&quot;m457b6cd672&quot; d=&quot;M -1.3 1.3 
L 1.3 1.3 
L 1.3 -1.3 
L -1.3 -1.3 
z
&quot;&gt;&lt;/path&gt;
    &lt;/defs&gt;
    &lt;g clip-path=&quot;url(#p037e41cebd)&quot;&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;308.872856&quot; y=&quot;147.066347&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;310.562554&quot; y=&quot;142.592537&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;312.252252&quot; y=&quot;140.355632&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;313.94195&quot; y=&quot;138.118726&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;314.505183&quot; y=&quot;133.644916&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;316.758113&quot; y=&quot;131.408011&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;319.574276&quot; y=&quot;120.223485&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;348.862374&quot; y=&quot;106.802054&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;358.437329&quot; y=&quot;104.565149&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;360.127027&quot; y=&quot;102.328244&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;360.69026&quot; y=&quot;97.854433&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;361.253492&quot; y=&quot;91.143718&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;361.816725&quot; y=&quot;84.433002&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;362.94319&quot; y=&quot;82.196097&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;363.506423&quot; y=&quot;79.959192&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;364.069656&quot; y=&quot;77.722287&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;365.196121&quot; y=&quot;75.485382&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;366.885819&quot; y=&quot;73.248477&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;369.701982&quot; y=&quot;71.011571&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;476.152953&quot; y=&quot;68.774666&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;482.911744&quot; y=&quot;64.300856&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;493.049932&quot; y=&quot;62.063951&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;494.176397&quot; y=&quot;59.827046&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;495.302863&quot; y=&quot;57.59014&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;496.992561&quot; y=&quot;55.353235&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;497.555793&quot; y=&quot;53.11633&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
     &lt;use xlink:href=&quot;#m457b6cd672&quot; x=&quot;499.808724&quot; y=&quot;48.64252&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/use&gt;
    &lt;/g&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_33&quot;&gt;
    &lt;path d=&quot;M 306.056693 151.540157 
L 306.056693 28.062992 
&quot; style=&quot;fill: none; stroke: var(--rule,currentColor); stroke-width: 0.5; stroke-linejoin: miter; stroke-linecap: square&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_34&quot;&gt;
    &lt;path d=&quot;M 306.056693 151.540157 
L 508.365354 151.540157 
&quot; style=&quot;fill: none; stroke: var(--rule,currentColor); stroke-width: 0.5; stroke-linejoin: miter; stroke-linecap: square&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_66&quot;&gt;
    &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: start; fill: currentColor; opacity: 0.75&quot; x=&quot;306.056693&quot; y=&quot;175.000819&quot; transform=&quot;rotate(-0 306.056693 175.000819)&quot;&gt;2025&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_67&quot;&gt;
    &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: start; fill: currentColor; opacity: 0.75&quot; x=&quot;343.775257&quot; y=&quot;175.000819&quot; transform=&quot;rotate(-0 343.775257 175.000819)&quot;&gt;2026&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_68&quot;&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor; opacity: 0.75&quot; x=&quot;508.365354&quot; y=&quot;25.593449&quot; transform=&quot;rotate(-0 508.365354 25.593449)&quot;&gt;squares: days with a first meeting (46 in all)&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_35&quot;&gt;
    &lt;path d=&quot;M 465.968443 54.06027 
Q 479.641964 55.705632 493.315484 57.350995 
&quot; style=&quot;fill: none; stroke: var(--rule,currentColor); stroke-width: 0.4; stroke-linecap: round&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_69&quot;&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 40.937686)&quot;&gt;Sep to Oct 2026&lt;/text&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 46.951807)&quot;&gt;a coding agent on my machine,&lt;/text&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 53.118467)&quot;&gt;Obsidian, skills, memory files, task&lt;/text&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 59.197041)&quot;&gt;notes, remote control, subagents&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_36&quot;&gt;
    &lt;path d=&quot;M 375.919915 92.496922 
Q 369.58392 91.912329 363.247925 91.327735 
&quot; style=&quot;fill: none; stroke: var(--rule,currentColor); stroke-width: 0.4; stroke-linecap: round&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_70&quot;&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 93.36698)&quot;&gt;Feb 2026&lt;/text&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 99.46489)&quot;&gt;HTML, a plan reviewed first, the terminal, npm,&lt;/text&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 105.479011)&quot;&gt;hosting, pip, Jupyter, virtual environments&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_37&quot;&gt;
    &lt;path d=&quot;M 375.931632 110.731543 
Q 363.386657 108.910461 350.841682 107.089379 
&quot; style=&quot;fill: none; stroke: var(--rule,currentColor); stroke-width: 0.4; stroke-linecap: round&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_71&quot;&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 115.00771)&quot;&gt;Jan 2026&lt;/text&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 121.10562)&quot;&gt;Git and GitHub, a cloud workspace, a database,&lt;/text&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 127.119741)&quot;&gt;APIs, a tracker&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_38&quot;&gt;
    &lt;path d=&quot;M 375.920325 139.230173 
Q 345.931704 138.692393 315.943083 138.154612 
&quot; style=&quot;fill: none; stroke: var(--rule,currentColor); stroke-width: 0.4; stroke-linecap: round&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_72&quot;&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 135.555011)&quot;&gt;Nov 2025&lt;/text&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 141.652921)&quot;&gt;context kept in files, Markdown, Python, an&lt;/text&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; fill: currentColor&quot; transform=&quot;translate(378.922101 147.731495)&quot;&gt;editor, a hosted notebook&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_73&quot;&gt;
    &lt;text style=&quot;font-size: 5.5px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor; opacity: 0.75&quot; x=&quot;422.927467&quot; y=&quot;78.616817&quot; transform=&quot;rotate(-0 422.927467 78.616817)&quot;&gt;practice; no first meeting in this record&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_74&quot;&gt;
    &lt;text style=&quot;font-weight: 700; font-size: 8px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;289.872&quot; y=&quot;21.545156&quot; transform=&quot;rotate(-0 289.872 21.545156)&quot;&gt;c&lt;/text&gt;
   &lt;/g&gt;
  &lt;/g&gt;
 &lt;/g&gt;
 &lt;defs&gt;
  &lt;clipPath id=&quot;p77b829a51f&quot;&gt;
   &lt;rect x=&quot;25.937008&quot; y=&quot;28.062992&quot; width=&quot;103.748031&quot; height=&quot;123.477165&quot;&gt;&lt;/rect&gt;
  &lt;/clipPath&gt;
  &lt;clipPath id=&quot;pff2633bbba&quot;&gt;
   &lt;rect x=&quot;165.99685&quot; y=&quot;28.062992&quot; width=&quot;103.748031&quot; height=&quot;123.477165&quot;&gt;&lt;/rect&gt;
  &lt;/clipPath&gt;
  &lt;clipPath id=&quot;p037e41cebd&quot;&gt;
   &lt;rect x=&quot;306.056693&quot; y=&quot;28.062992&quot; width=&quot;202.308661&quot; height=&quot;123.477165&quot;&gt;&lt;/rect&gt;
  &lt;/clipPath&gt;
 &lt;/defs&gt;
&lt;/svg&gt;&lt;/p&gt;
&lt;h2 id=&quot;where-i-started&quot;&gt;Where I started&lt;/h2&gt;
&lt;p&gt;I had two objections, and they were of different kinds.&lt;/p&gt;
&lt;p&gt;The first was usefulness. Chemical engineering gave me a modeling habit before it gave me any data. My first project in graduate school was a steady-state flowsheet in Aspen Plus, where every stream was computed and nothing was measured, and the experimental work that followed was the opposite, real measurements and a mass balance that closed on good days. Neither needed a learning algorithm, and for most of that decade I read about neural networks the way I read about fusion. The value of my work sits in a measured number, and I did not see what a language model could add to that.&lt;/p&gt;
&lt;p&gt;The second was cost. I work on fuels and energy, and a technology that needs new power stations for its data centres did not look like something an engineer should use casually. The International Energy Agency puts data centres at around 1.5 percent of the world’s electricity in 2024, about 415 terawatt-hours, and expects that to more than double, to around 945 terawatt-hours, by 2030, with AI as the largest driver of the growth.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;International Energy Agency (2025), executive summary: data centres accounted for around 1.5% of the world’s electricity consumption in 2024, or 415 TWh, and consumption is set to more than double to around 945 TWh by 2030, with AI the most important driver. &lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-changed-my-mind&quot;&gt;What changed my mind&lt;/h2&gt;
&lt;p&gt;A friend of mine, a graduate student in the Department of Civil Engineering at UW, convinced me to try it and get started. So I started, in November 2025, having never written a line of Python. The first obstacle was the one every beginner meets, what to install and why it does not run. A hosted notebook took that question away. Google Colab gave me a working Python cell in a browser with nothing installed, and my first physics-informed neural network, on a first-order reactor, ran the same evening. Within a week I had a reading list that went from linear regression to gradient boosting, written for me in the order I needed it.&lt;/p&gt;
&lt;p&gt;The second step was the opposite of the first. In February 2026 I moved the work onto my own machine: Python, an editor, a virtual environment, Jupyter. That month is the tall bar in the figure. Most of those thousand messages were error texts: a package name I had mistyped, a Windows alias that pretended to be Python, a search that ran overnight and wrote nothing to disk. Each cost an evening, and each is now a short note on this site. What I learned in that month is where an assistant earns its place: beside work I am doing myself, one cell at a time, explaining the error in front of me. When I let it run the whole analysis in its own window, I learned nothing and could check nothing.&lt;/p&gt;
&lt;p&gt;From March to the end of August the curve in the figure is flat. The record behind the figure is my sessions with one assistant, and in those months I was not using it, so the record shows no new tool there. I used other tools, and I read books and articles and took courses, and I practiced on what I had met in February. None of that is in this record.&lt;/p&gt;
&lt;p&gt;The third step came in the autumn, when I opened a coding agent in a folder on my own computer. At this stage prompting has little to do with it. The work is to read every line the model writes and run it and keep nothing I cannot explain. The model is fast and confident, and the second property is the dangerous one.&lt;/p&gt;
&lt;h2 id=&quot;what-i-did-about-the-cost&quot;&gt;What I did about the cost&lt;/h2&gt;
&lt;p&gt;The second objection I did not resolve, so I put numbers beside it.&lt;/p&gt;
&lt;p&gt;Google has published an estimate for its own assistant: the median text prompt uses 0.24 watt-hours of energy, emits 0.03 grams of carbon dioxide equivalent and consumes 0.26 millilitres of water, and by the company’s account the energy per prompt fell by a factor of 33 over twelve months.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Vahdat and Dean (2025): the median Gemini Apps text prompt is estimated at 0.24 Wh, 0.03 g CO2e and 0.26 mL of water, with energy per median prompt down 33 times over a recent twelve-month period. The figures are the company’s own. &lt;/span&gt; That is a company measuring its own product, and it covers a median text prompt, not a long session with a large model. In September 2026 I drew energy per query against model size as an animated chart, to see the shape for myself: the larger the model, the more each answer costs.&lt;/p&gt;
&lt;p&gt;&lt;svg class=&quot;plot bars figure-svg&quot; role=&quot;img&quot; aria-label=&quot;Electricity used by data centres in 2024 and expected in 2030, from the IEA, in terawatt-hours&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; width=&quot;252.283465pt&quot; height=&quot;69.165354pt&quot; viewBox=&quot;0 0 252.283465 69.165354&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; version=&quot;1.1&quot;&gt;
 &lt;defs&gt;
  &lt;style type=&quot;text/css&quot;&gt;*{stroke-linejoin: round; stroke-linecap: butt}&lt;/style&gt;
 &lt;/defs&gt;
 &lt;g id=&quot;figure_1&quot;&gt;
  &lt;g id=&quot;patch_1&quot;&gt;
   &lt;path d=&quot;M 0 69.165354 
L 252.283465 69.165354 
L 252.283465 0 
L 0 0 
L 0 69.165354 
z
&quot; style=&quot;fill: none&quot;&gt;&lt;/path&gt;
  &lt;/g&gt;
  &lt;g id=&quot;axes_1&quot;&gt;
   &lt;g id=&quot;patch_2&quot;&gt;
    &lt;path d=&quot;M 85.776378 46.488189 
L 227.055118 46.488189 
L 227.055118 15.590551 
L 85.776378 15.590551 
L 85.776378 46.488189 
z
&quot; style=&quot;fill: none&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_3&quot;&gt;
    &lt;path d=&quot;M 85.776378 45.083751 
L 220.327559 45.083751 
L 220.327559 34.550465 
L 85.776378 34.550465 
z
&quot; clip-path=&quot;url(#p546c17de86)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_4&quot;&gt;
    &lt;path d=&quot;M 85.776378 27.528275 
L 144.864992 27.528275 
L 144.864992 16.994989 
L 85.776378 16.994989 
z
&quot; clip-path=&quot;url(#p546c17de86)&quot; style=&quot;fill: var(--accent,currentColor)&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;matplotlib.axis_1&quot;&gt;
    &lt;g id=&quot;xtick_1&quot;&gt;
     &lt;g id=&quot;line2d_1&quot;&gt;
      &lt;defs&gt;
       &lt;path id=&quot;m3454c6128e&quot; d=&quot;M 0 0 
L 0 2.5 
&quot; style=&quot;stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/path&gt;
      &lt;/defs&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m3454c6128e&quot; x=&quot;85.776378&quot; y=&quot;46.488189&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_1&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;85.776378&quot; y=&quot;56.782876&quot; transform=&quot;rotate(-0 85.776378 56.782876)&quot;&gt;0&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_2&quot;&gt;
     &lt;g id=&quot;line2d_2&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m3454c6128e&quot; x=&quot;114.252818&quot; y=&quot;46.488189&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_2&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;114.252818&quot; y=&quot;56.782876&quot; transform=&quot;rotate(-0 114.252818 56.782876)&quot;&gt;200&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_3&quot;&gt;
     &lt;g id=&quot;line2d_3&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m3454c6128e&quot; x=&quot;142.729259&quot; y=&quot;46.488189&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_3&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;142.729259&quot; y=&quot;56.782876&quot; transform=&quot;rotate(-0 142.729259 56.782876)&quot;&gt;400&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_4&quot;&gt;
     &lt;g id=&quot;line2d_4&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m3454c6128e&quot; x=&quot;171.205699&quot; y=&quot;46.488189&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_4&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;171.205699&quot; y=&quot;56.782876&quot; transform=&quot;rotate(-0 171.205699 56.782876)&quot;&gt;600&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;xtick_5&quot;&gt;
     &lt;g id=&quot;line2d_5&quot;&gt;
      &lt;g&gt;
       &lt;use xlink:href=&quot;#m3454c6128e&quot; x=&quot;199.68214&quot; y=&quot;46.488189&quot; style=&quot;fill: var(--rule,currentColor); stroke: var(--rule,currentColor); stroke-width: 0.5&quot;&gt;&lt;/use&gt;
      &lt;/g&gt;
     &lt;/g&gt;
     &lt;g id=&quot;text_5&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;199.68214&quot; y=&quot;56.782876&quot; transform=&quot;rotate(-0 199.68214 56.782876)&quot;&gt;800&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;text_6&quot;&gt;
     &lt;text style=&quot;font-size: 7px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: middle; fill: currentColor&quot; x=&quot;156.415748&quot; y=&quot;66.985845&quot; transform=&quot;rotate(-0 156.415748 66.985845)&quot;&gt;terawatt-hours in the year&lt;/text&gt;
    &lt;/g&gt;
   &lt;/g&gt;
   &lt;g id=&quot;matplotlib.axis_2&quot;&gt;
    &lt;g id=&quot;ytick_1&quot;&gt;
     &lt;g id=&quot;line2d_6&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_7&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;82.276378&quot; y=&quot;41.973358&quot; transform=&quot;rotate(-0 82.276378 41.973358)&quot;&gt;2030, expected&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
    &lt;g id=&quot;ytick_2&quot;&gt;
     &lt;g id=&quot;line2d_7&quot;&gt;&lt;/g&gt;
     &lt;g id=&quot;text_8&quot;&gt;
      &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: end; fill: currentColor&quot; x=&quot;82.276378&quot; y=&quot;24.408976&quot; transform=&quot;rotate(-0 82.276378 24.408976)&quot;&gt;2024&lt;/text&gt;
     &lt;/g&gt;
    &lt;/g&gt;
   &lt;/g&gt;
   &lt;g id=&quot;patch_5&quot;&gt;
    &lt;path d=&quot;M 85.776378 46.488189 
L 227.055118 46.488189 
&quot; style=&quot;fill: none; stroke: var(--rule,currentColor); stroke-width: 0.5; stroke-linejoin: miter; stroke-linecap: square&quot;&gt;&lt;/path&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_9&quot;&gt;
    &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: start; fill: currentColor&quot; x=&quot;220.327559&quot; y=&quot;41.368202&quot; transform=&quot;rotate(-0 220.327559 41.368202)&quot;&gt; 945&lt;/text&gt;
   &lt;/g&gt;
   &lt;g id=&quot;text_10&quot;&gt;
    &lt;text style=&quot;font-size: 6px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: start; fill: currentColor&quot; x=&quot;144.864992&quot; y=&quot;23.812726&quot; transform=&quot;rotate(-0 144.864992 23.812726)&quot;&gt; 415&lt;/text&gt;
   &lt;/g&gt;
  &lt;/g&gt;
  &lt;g id=&quot;text_11&quot;&gt;
   &lt;text style=&quot;font-weight: 700; font-size: 7px; font-family: &amp;#x27;Arial&amp;#x27;, &amp;#x27;Helvetica&amp;#x27;, &amp;#x27;DejaVu Sans&amp;#x27;, sans-serif; text-anchor: start; fill: currentColor&quot; x=&quot;2.522835&quot; y=&quot;7.085429&quot; transform=&quot;rotate(-0 2.522835 7.085429)&quot;&gt;Electricity used by data centres, IEA&lt;/text&gt;
  &lt;/g&gt;
 &lt;/g&gt;
 &lt;defs&gt;
  &lt;clipPath id=&quot;p546c17de86&quot;&gt;
   &lt;rect x=&quot;85.776378&quot; y=&quot;15.590551&quot; width=&quot;141.27874&quot; height=&quot;30.897638&quot;&gt;&lt;/rect&gt;
  &lt;/clipPath&gt;
 &lt;/defs&gt;
&lt;/svg&gt;&lt;/p&gt;
&lt;p&gt;So two things are true at the same time. The cost of one question is small and falling. The total is large and rising, because use grows faster than efficiency improves. I use these tools every working day now, and I count the objection as open.&lt;/p&gt;
&lt;h2 id=&quot;what-the-figure-shows&quot;&gt;What the figure shows&lt;/h2&gt;
&lt;p&gt;All three panels are drawn from one source, my sessions with an assistant, and they measure only that. Panel a counts the days in each month on which I had at least one working session: 193 days in a little under twelve months, with February and September at 26 each and June at 4. Panel b counts the messages I wrote, and February alone, with a little over a thousand, holds more than the three months after it combined, which is what learning to program looks like from the inside. Panel c is the learning curve. It counts the tools, methods and working habits I met for the first time, 46 in all, and it has the shape of a staircase with one long landing. It climbs from November to February, when 36 of the 46 arrived. From March to the end of August it is flat, with one first meeting on the last days of August. The flat stretch is not idle time. The bars in panels a and b run through it, which is practice on what I had already met, and it also holds the other tools and the reading that a record of my sessions with one assistant does not see. The curve rises again in September. Named beside it, month by month, are the things I had to learn before I could work with an agent on my own machine, in a terminal and an editor.&lt;/p&gt;
&lt;p&gt;The figure is a count of days and first meetings in one kind of record. It is not a measure of competence, and it is not the whole of the year.&lt;/p&gt;
&lt;h2 id=&quot;working-with-an-agent-did-not-start-overnight&quot;&gt;Working with an agent did not start overnight&lt;/h2&gt;
&lt;p&gt;The last squares on the curve are the ones people ask about: a coding agent running on my own machine, with skills, memory files and subagents. It looks like a sudden change, and it was not. Each of those rests on something earlier on the same line.&lt;/p&gt;
&lt;p&gt;In November 2025 I learned to keep context in files and not in the chat, because a long conversation forgets and a file does not. In January 2026 a project with an assistant in a cloud workspace forced Git, a repository and a tracker on me, since the file list had outgrown the conversation. In February came the terminal, the package manager, a virtual environment and a notebook on my own machine, and the habit of having a plan reviewed and criticized before anything is built. By August I had met a database, an API and a deployment.&lt;/p&gt;
&lt;p&gt;Only then, in September 2026, did I open a coding agent in a folder on my own computer. What made it usable in the first week was everything above: I could read what it wrote, I knew what a commit was for, I knew why the environment mattered, and I already had the habit of asking for the plan first. Skills, which are saved procedures, and memory files, which are saved context, are the same two habits from November and February given names by the tool.&lt;/p&gt;
&lt;p&gt;An engineer who starts with the agent and skips the year will get output sooner than I did. I do not think they will be able to check it.&lt;/p&gt;
&lt;h2 id=&quot;where-that-leaves-me&quot;&gt;Where that leaves me&lt;/h2&gt;
&lt;p&gt;A year ago I would have said that I ask a chat assistant for a draft or a summary now and then, and I would have defended staying there. Now I run models on my own hardware and I train and validate models on small tables of my own. I also work with a coding agent in a folder on my computer. Training a model on a small table is easy. Validating the result is the whole problem, and that is where my effort goes.&lt;/p&gt;
&lt;p&gt;I have not published a machine learning method or result that someone else can reproduce, and I have not shipped anything that runs without me. What I can do is also narrow. I can handle a small table of runs, and for everything else I am still the person who asks the assistant. Physics-informed networks for reactor models are on my list because the problem fits. Özdemir and Yildirim describe the gap when they note that a purely data-driven model has no knowledge of thermodynamic limits and nothing stops it from predicting a conversion above equilibrium.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;Özdemir and Yildirim (2025), p. 10, on hybrid and physics-informed models. &lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-i-still-do-not-trust&quot;&gt;What I still do not trust&lt;/h2&gt;
&lt;p&gt;An assistant that is asked about a specific published number is wrong often enough that I read the source for every number I use, without exception. Al Sharah and colleagues put the general version of this well when they warn that easy access lets people apply methods they do not understand.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;Al Sharah et al. (2024), pp. 623 to 624, under “challenges and drawbacks”. &lt;/span&gt; The warning applies to the chat window as much as to a random forest.&lt;/p&gt;
&lt;p&gt;I also do not trust my own speed. A year of this has made me faster at producing things, and speed checks nothing. The check I keep is the one from February: if I cannot run it and explain it, it does not go in.&lt;/p&gt;
&lt;h2 id=&quot;for-a-reader-who-has-not-started&quot;&gt;For a reader who has not started&lt;/h2&gt;
&lt;p&gt;The first step that worked for me was the cheapest one, a hosted notebook with nothing installed and one real problem from my own field. The pieces in the &lt;a href=&quot;/series/from-zero/&quot;&gt;From zero&lt;/a&gt; series are the notes I would have wanted in that first month, in the order I needed them.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;International Energy Agency (2025), executive summary: data centres accounted for around 1.5% of the world’s electricity consumption in 2024, or 415 TWh, and consumption is set to more than double to around 945 TWh by 2030, with AI the most important driver. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Vahdat and Dean (2025): the median Gemini Apps text prompt is estimated at 0.24 Wh, 0.03 g CO2e and 0.26 mL of water, with energy per median prompt down 33 times over a recent twelve-month period. The figures are the company’s own. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Özdemir and Yildirim (2025), p. 10, on hybrid and physics-informed models. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Al Sharah et al. (2024), pp. 623 to 624, under “challenges and drawbacks”. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Linux beside Aspen Plus</title>
    <link href="https://tishad.net/notes/linux-beside-aspen-plus/" />
    <id>https://tishad.net/notes/linux-beside-aspen-plus/</id>
    <published>2026-10-11T00:00:00.000Z</published>
    <updated>2026-10-11T00:00:00.000Z</updated>
    <summary>Why the AI and data stack lives in a Linux layer under Windows rather than replacing it, and what that layer holds.</summary>
    <content type="html">&lt;p&gt;Aspen Plus runs on Windows and nowhere else, and that one fact shaped the whole layout of &lt;a href=&quot;/notes/local-ai-stack/&quot;&gt;the workstation&lt;/a&gt;. A process engineer who gives up the steady-state simulator to get a cleaner machine learning environment has traded the wrong thing.&lt;/p&gt;
&lt;p&gt;The choice was between three layouts. A dual boot keeps both systems but makes switching a reboot, which is enough friction that one side goes unused. A live USB is a demonstration, not a workstation. The Windows Subsystem for Linux, version 2, runs a real Ubuntu kernel inside Windows, shares the file system, and passes the GPU through, so CUDA work in Linux sees the same RTX 4090 that the Windows tools see.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;NVIDIA’s CUDA on WSL guide describes GPU passthrough to the WSL2 kernel; the driver lives on the Windows side and the CUDA toolkit inside Ubuntu. &lt;/span&gt; That is the layout in use: Windows 11 on the metal, Ubuntu 24.04 in the subsystem, Aspen Plus on one side and everything data-shaped on the other.&lt;/p&gt;
&lt;p&gt;The Linux side holds the Python environment, managed with uv or conda, carrying the engineering stack I already had: scikit-learn, XGBoost and SHAP for tabular models, SIPPY for system identification, CasADi and Pyomo for optimization, BioSTEAM for techno-economic models. It also holds Docker, which runs Open WebUI, and will hold Ollama when the move from LM Studio happens.&lt;/p&gt;
&lt;p&gt;Time-series data from a pilot unit arrive as thousands of tags at thirty-second intervals over weeks, which is tens of millions of rows per campaign in the worst case. Excel is the wrong container above a few hundred thousand rows and a database server is more than one person needs. Parquet files queried with DuckDB sit in between: columnar, compressed, fast to scan, and readable from Python without a running service.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;DuckDB reads Parquet directly and runs analytical SQL in process; the documentation covers the Python package and the Parquet reader. &lt;/span&gt; The pattern is general and nothing in it is specific to any one unit.&lt;/p&gt;
&lt;p&gt;What has surprised me is how little I miss on the Windows side. The simulator, the office tools and the vault application stay where they are. The terminal, the models and the data moved. The two meet at a shared folder, and a result computed in Linux opens in Excel without ceremony.&lt;/p&gt;
&lt;p&gt;Still open is the laptop. A Surface Pro cannot run this layout well, and the question is whether the next portable machine is a Linux laptop that reaches the desktop over Tailscale or a second Windows machine with the same subsystem.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;NVIDIA’s CUDA on WSL guide describes GPU passthrough to the WSL2 kernel; the driver lives on the Windows side and the CUDA toolkit inside Ubuntu. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;DuckDB reads Parquet directly and runs analytical SQL in process; the documentation covers the Python package and the Parquet reader. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>The local AI stack</title>
    <link href="https://tishad.net/notes/local-ai-stack/" />
    <id>https://tishad.net/notes/local-ai-stack/</id>
    <published>2026-10-11T00:00:00.000Z</published>
    <updated>2026-10-11T00:00:00.000Z</updated>
    <summary>What runs on the home workstation, what is planned, and the one rule that decides what goes on it.</summary>
    <content type="html">&lt;p&gt;One rule decides everything on this machine: nothing leaves it, and nothing that belongs to my employer arrives on it. The first half is a technical constraint and the second is a policy I set for myself. Local processing does not make someone else’s data mine to carry home.&lt;/p&gt;
&lt;p&gt;The hardware is a 2024 desktop with an Intel Core i9, an NVIDIA RTX 4090 with 24 GB of video memory, 64 GB of system memory in two sticks, and an MSI Z790 Edge board. The 4090 is the reason the rest exists. A 32 billion parameter model in 4-bit quantization fits in its memory with room for a usable context window, and that size is where open models stopped feeling like toys for engineering questions.&lt;/p&gt;
&lt;p&gt;What runs today is LM Studio as the chat interface and model server, with AnythingLLM beside it for retrieval over my literature folder (&lt;a href=&quot;/notes/private-retrieval-over-papers/&quot;&gt;the retrieval note&lt;/a&gt; covers that). The models are Qwen3 32B for reasoning, Qwen2.5-Coder 32B for code, and bge-m3 for embeddings. The embedding choice mattered more than I expected: retrieval quality moved more when I changed the embedding model than when I changed the chat model.&lt;/p&gt;
&lt;p&gt;What is planned is the move from Windows-native tools to a Linux layer with Ollama serving the same models, Open WebUI in Docker as the interface, and Aider or Continue in VS Code for coding with a local model (&lt;a href=&quot;/notes/linux-beside-aspen-plus/&quot;&gt;the Linux note&lt;/a&gt; explains why it is a layer and not a replacement). Tailscale gives remote access from the laptop without opening a port.&lt;/p&gt;
&lt;p&gt;What is excluded is any tool that needs a cloud API, which rules out the agentic coding assistant I use for this site. That is a real cost. The local coding models are a year or two behind, and I accept the gap on this machine because the whole point of it is the first rule.&lt;/p&gt;
&lt;p&gt;What I have not done is fine-tune anything. The use cases so far are chat and writing over my own notes, code, and small machine learning jobs on tabular data, and none of them has needed a model trained on my material rather than a model reading it.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Private retrieval over my own papers</title>
    <link href="https://tishad.net/notes/private-retrieval-over-papers/" />
    <id>https://tishad.net/notes/private-retrieval-over-papers/</id>
    <published>2026-10-11T00:00:00.000Z</published>
    <updated>2026-10-11T00:00:00.000Z</updated>
    <summary>Retrieval-augmented chat over a folder of PDFs on the local machine, what it is good for, what it is not, and the reading rule that makes it safe to use.</summary>
    <content type="html">&lt;p&gt;A folder of about forty PDFs is the whole corpus, and the question I ask of it most often is “where did I read that”. Retrieval-augmented generation answers that question well and answers almost nothing else reliably.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Lewis et al. (2020) introduced the retrieval-augmented pattern: retrieve passages by vector similarity, then generate conditioned on them. The failure modes I describe follow from the pattern, not from any one implementation. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The setup is AnythingLLM pointed at the folder, with bge-m3 producing the embeddings and a local Qwen3 32B model writing the answers, all on &lt;a href=&quot;/notes/local-ai-stack/&quot;&gt;the workstation&lt;/a&gt;. Each PDF is split into chunks of a few hundred words, each chunk becomes a vector, and a question is matched against those vectors before the model sees anything. The model then writes from the matched chunks and cites them.&lt;/p&gt;
&lt;p&gt;What it is good for is locating. Asked which of the reviews gives a threshold for how many data points a molecular model needs, it returns the right paper and the right page in a few seconds, which is faster than my own memory and much faster than reopening five PDFs. It is also good for the first draft of a reference list and for finding every place a term appears across the corpus.&lt;/p&gt;
&lt;p&gt;What it is not good for is the number itself. A retrieved chunk can be the wrong table, a value can be transcribed with the wrong unit, and the model will state the result with the same confidence either way. The rule that makes the tool safe is older than the tool: no number, claim or quotation goes into anything I write until I have read it in the source. Retrieval tells me where to read. It does not replace the reading.&lt;/p&gt;
&lt;p&gt;Two practical findings. The embedding model matters more than the chat model; changing from a general English embedding to bge-m3 improved the hit rate on engineering terms more than any change of chat model did. And chunk size is a trade: short chunks find a sentence, long chunks keep a table together, and tables are where chemical engineering papers keep their results, so I lean long.&lt;/p&gt;
&lt;p&gt;Open for me is whether a corpus of forty papers is worth this machinery at all. At forty, a good folder structure and a reading note per paper do most of the work. At four hundred the retrieval would earn its keep. The reading notes in the &lt;a href=&quot;/library/&quot;&gt;Library&lt;/a&gt; are the part of the system that scales either way.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Lewis et al. (2020) introduced the retrieval-augmented pattern: retrieve passages by vector similarity, then generate conditioned on them. The failure modes I describe follow from the pattern, not from any one implementation. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Telling a skill exactly which files it may read</title>
    <link href="https://tishad.net/log/telling-a-skill-exactly-which-files-it-may-read/" />
    <id>https://tishad.net/log/telling-a-skill-exactly-which-files-it-may-read/</id>
    <published>2026-10-04T00:00:00.000Z</published>
    <updated>2026-10-04T00:00:00.000Z</updated>
    <summary>My literature note skill in Claude Code was reading through my notes vault for every paper, so I had it rewritten to read only its instructions, the template and the PDF text.</summary>
    <content type="html">&lt;p&gt;I keep literature notes on journal articles in my Obsidian vault, and I have a skill in Claude Code that writes them from the PDF. A &lt;a href=&quot;https://code.claude.com/docs/en/skills&quot;&gt;skill&lt;/a&gt; is a folder with a file of written instructions in it, and Claude follows those instructions when I run the command. I drop PDFs into an inbox folder and run the command, and it writes a note for each paper that does not have one yet.&lt;/p&gt;
&lt;p&gt;This time there was one new paper waiting, a 33 page review. Claude read the whole paper. Then it went through my vault to pull context for a Connections section in the note, and on wrap up it also added a dated entry to one of my hub notes and made a Log section there, because the hub did not have one.&lt;/p&gt;
&lt;p&gt;I was watching the run and I noticed the agent reading a lot of files. Every file it reads costs tokens. I think a skill reads whatever it can reach unless its instructions say what to read. So I told Claude what I want from this skill. I just want it to extract the data from the PDF and make the Markdown file in the literature folder inside my vault. I do not want it to consume any more tokens than it absolutely has to to do the job accurately.&lt;/p&gt;
&lt;p&gt;Claude rewrote the skill so that it reads only its own instructions, the note template and the text of the PDF. It also added a small script, &lt;code&gt;extract.sh&lt;/code&gt;, that pulls the text out of the PDF, and it saved the rule to its memory.&lt;/p&gt;
&lt;p&gt;It had tested the extraction step on the PDFs in the inbox but not the full rewritten skill. So I ran the command again, and it wrote a note from the one PDF that did not have a note yet. I did not measure the tokens before and after.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Testing a skill with planted errors and a run without the skill</title>
    <link href="https://tishad.net/essays/testing-a-skill-with-planted-errors/" />
    <id>https://tishad.net/essays/testing-a-skill-with-planted-errors/</id>
    <published>2026-10-03T00:00:00.000Z</published>
    <updated>2026-10-03T00:00:00.000Z</updated>
    <summary>Claude built a unit checking skill for me and tested it on a calculation sheet with four planted errors, on two models, with and without the skill.</summary>
    <content type="html">&lt;p&gt;I asked Claude Code to make a skill that checks the units on a calculation sheet. A skill is a file of written instructions, a &lt;code&gt;SKILL.md&lt;/code&gt;, that Claude uses when the task calls for it or when you type its name as a command.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Anthropic (2026), “Extend Claude with skills”, Claude Code documentation. &lt;/span&gt; It is like an SOP, but for the assistant.&lt;/p&gt;
&lt;p&gt;Along with the instructions it wrote a script, &lt;code&gt;check_units.py&lt;/code&gt;, that does the conversions, with a test where 15 of 15 values agree when they are worked out by two routes. It wrote two reference sheets, one of unit factors and one of unit traps. And it checked its sources first: the conversion factors in NIST SP 811,&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Thompson and Taylor (2008), NIST Special Publication 811, Appendix B, “Conversion Factors”. &lt;/span&gt; the licence of pint, which is a Python library for units, and the NASA report on the Mars Climate Orbiter. That is the spacecraft that was lost because thruster data came in pound-force seconds where the specification asked for newton seconds, so the numbers were off by a factor of 4.45.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;Mars Climate Orbiter Mishap Investigation Board (1999), Phase I Report, section 4, “MCO Root Cause”, p. 16. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Then it made the test, a reference problem, which is a short calculation sheet with four errors planted in it, and five pass criteria written down with it. The four errors were a gallon conversion, a temperature difference, a site atmospheric pressure, and a missing gc. If you have not worked in US customary units, gc is the constant that connects pound-mass and pound-force in an equation, 32.174 lbm ft per lbf s squared, and if you leave it out your answer is wrong by a factor of 32.17.&lt;/p&gt;
&lt;p&gt;To me this is like testing a method in the lab, where you make a sample that you already know the answer for and give it to somebody who does not know the answer. Here the somebody was a fresh assistant each time, one that had seen nothing of the work. Some trials had the skill and some did not, and some were allowed to run the script while the others had to do the arithmetic by hand. It used two Claude models, Haiku, which is the small one, and Fable 5.1, which is the strong one.&lt;/p&gt;
&lt;p&gt;In the first round, Haiku with no skill caught three of the four errors, the gallon conversion, the temperature difference and the atmospheric pressure, and missed the missing gc. Fable 5.1 with no skill found all four. So the strong model passes this problem without the skill. Haiku with the skill, working by hand, caught the missing gc and named the 32.17 ratio as the sign of it. Fable 5.1 with the skill and the script passed every criterion and also returned ten rough edges in the skill text.&lt;/p&gt;
&lt;p&gt;So Claude revised the skill and ran the trials again. Haiku by hand had two defects in round one: it had not answered all four conversions, and its values did not all carry both the sheet unit and the SI unit. Both were fixed after the revision, and it found all four errors again, but it still failed. Fable 5.1 passed by hand and with the script. The Haiku run with the script ended in an API error, and the partial report it left had missed gc again. So on the small model the catch did not hold every time.&lt;/p&gt;
&lt;p&gt;After that the skill text was frozen at 169 lines and a final round was run on the frozen text. Fable 5.1 passed by hand and with the script. Haiku 4.5 passed with the script. Haiku by hand found all the errors and failed one criterion, because a single SI value was 1.9 percent off.&lt;/p&gt;
&lt;p&gt;Before trusting the record Claude audited the transcripts of the runs, because a stray file from one of the earlier runs had landed in the skill folder itself, so a trial could have seen the expected answers.&lt;/p&gt;
&lt;p&gt;So the strong model did not need the skill on this problem, and the small one gained from it, though not reliably. All of that is one problem and two models from one company, and the skill still needs a run on another vendor’s assistant and my own review.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Anthropic (2026), “Extend Claude with skills”, Claude Code documentation. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Thompson and Taylor (2008), NIST Special Publication 811, Appendix B, “Conversion Factors”. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Mars Climate Orbiter Mishap Investigation Board (1999), Phase I Report, section 4, “MCO Root Cause”, p. 16. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Prediction and interpretation are two different uses of a fitted model</title>
    <link href="https://tishad.net/essays/prediction-and-interpretation-are-two-different-uses/" />
    <id>https://tishad.net/essays/prediction-and-interpretation-are-two-different-uses/</id>
    <published>2026-08-23T00:00:00.000Z</published>
    <updated>2026-08-23T00:00:00.000Z</updated>
    <summary>A fitted model can forecast an output or be asked which inputs it relies on, and this is the vocabulary for the second use with the limits its own documentation gives.</summary>
    <content type="html">&lt;p&gt;I was trying to get my thoughts straight on what machine learning is for when the data come from experiments, so that I could say it in my own words. I had already asked an assistant what machine learning is used for in experimental research and development like mine, in chemistry and chemical engineering. Then I dictated my own version. Machine learning offers predictive modeling of datasets. On large experimental datasets the models fit well. On pilot data the same models do not, or at least it is not guaranteed. And for pilot-scale or process development work, prediction is not the only thing we want anyway. We want the relationships between the variables, the importance of each one, and the effect of each on the final product.&lt;/p&gt;
&lt;p&gt;So there are two uses in that paragraph, and each has its own words. A fitted model is a function learned from a table. Each row is a run, the input columns are called features (temperature, feed type, reactor configuration) and the output column is called the target or the label (a yield). Prediction is the use everybody knows. You give the function a new row of features and read off the target. The other use is called interpretation, or model inspection. You ask the fitted function which features it leans on and in which direction, and you do not predict any new row.&lt;/p&gt;
&lt;p&gt;This split is not new. Breiman in 2001 described two cultures of statistical modeling, one that assumes the data come from a stated model and reads its parameters, and one that treats the mechanism as unknown and judges an algorithm by how well it predicts.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Breiman (2001), abstract: the data modeling culture and the algorithmic modeling culture. &lt;/span&gt; Shmueli in 2010 argued that explaining and predicting are different goals that people mix up, and that the difference affects every step of the modeling.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Shmueli (2010), abstract: explanation and prediction as distinct purposes, and the conflation of explanatory with predictive power. &lt;/span&gt; I think an engineer already knows this split in a simpler form. A calibration curve is for predicting, because you read the signal and get the concentration. A regression of yield on temperature in a design of experiments is for explaining, because you want to know whether the slope is there and how large it is.&lt;/p&gt;
&lt;p&gt;Interpretation has its own tools, and these are the ones I met.&lt;/p&gt;
&lt;p&gt;Permutation importance takes a model that is already fitted, shuffles one feature column so it no longer lines up with the rows, and measures how much the score drops.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;scikit-learn user guide, “Permutation feature importance”: the definition, the warning on low-importance features of a bad model, the statement that importance is specific to a particular model, and the note on correlated features.   &lt;/span&gt; A big drop means the model was relying on that column.&lt;/p&gt;
&lt;p&gt;SHAP values come from a 2017 paper by Lundberg and Lee. The method gives each feature an importance value for one particular prediction, so the prediction for one run is split into a contribution from each input.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;Lundberg and Lee (2017), abstract: SHAP assigns each feature an importance value for a particular prediction. &lt;/span&gt; Averaged over all the runs that gives a ranking, and plotted against a feature it shows which way the effect goes.&lt;/p&gt;
&lt;p&gt;An ablation study I had to ask about directly, what it is and what it tells us. It takes something out, an input or a part of the method, fits again, and compares the result with and without it. I think of it as running the unit with one stream shut off.&lt;/p&gt;
&lt;p&gt;The documentation for these tools also says where they stop.&lt;/p&gt;
&lt;p&gt;The scikit-learn guide says permutation importance shows how important a feature is for one particular model, and that a feature with low importance in a bad model could be very important in a good one, so the model has to predict well on held-out data before you read its importances.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;scikit-learn user guide, “Permutation feature importance”: the definition, the warning on low-importance features of a bad model, the statement that importance is specific to a particular model, and the note on correlated features.   &lt;/span&gt; So the two uses are tied together. If the model predicts badly, its ranking of the inputs means very little.&lt;/p&gt;
&lt;p&gt;Then there are inputs that move together. If two features are correlated and you shuffle one, the model still gets the same information through the other, and both come out lower than they deserve.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;scikit-learn user guide, “Permutation feature importance”: the definition, the warning on low-importance features of a bad model, the statement that importance is specific to a particular model, and the note on correlated features.   &lt;/span&gt; In process data, two settings that were always changed together cannot be pulled apart by any ranking.&lt;/p&gt;
&lt;p&gt;And a high importance does not mean cause. scikit-learn has an example called “Failure of Machine Learning to infer causal effects”, where a model scores almost as well with a variable left out and still gives a wrong answer about the effect of the one that stayed in.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-5&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;5&lt;/span&gt;scikit-learn example, “Failure of Machine Learning to infer causal effects”, including its lessons learned. &lt;/span&gt; The way out it points to is one an experimentalist already has, which is an experiment where you set that variable on purpose.&lt;/p&gt;
&lt;p&gt;Where I have got to is that for pilot data I use the model mostly to find which variables matter and how, and much less to forecast. But one question still bothers me. If prediction accuracy is not the main goal of experimental and pilot-scale R&amp;#x26;D, then what is?&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Breiman (2001), abstract: the data modeling culture and the algorithmic modeling culture. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Shmueli (2010), abstract: explanation and prediction as distinct purposes, and the conflation of explanatory with predictive power. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;scikit-learn user guide, “Permutation feature importance”: the definition, the warning on low-importance features of a bad model, the statement that importance is specific to a particular model, and the note on correlated features. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Lundberg and Lee (2017), abstract: SHAP assigns each feature an importance value for a particular prediction. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;scikit-learn example, “Failure of Machine Learning to infer causal effects”, including its lessons learned. &lt;a href=&quot;#user-content-fnref-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>A venv does not survive being copied to another machine</title>
    <link href="https://tishad.net/log/a-venv-does-not-survive-being-copied-to-another-machine/" />
    <id>https://tishad.net/log/a-venv-does-not-survive-being-copied-to-another-machine/</id>
    <published>2026-02-21T00:00:00.000Z</published>
    <updated>2026-02-21T00:00:00.000Z</updated>
    <summary>I copied a Python project to a new Windows computer with its virtual environment inside, pip failed with a launcher error, and I had to delete the venv and build a new one.</summary>
    <content type="html">&lt;p&gt;I was running a Python project in a Jupyter notebook, and a run died when I closed the browser by accident. After that I moved the work to a new computer. I copied the whole project folder across to the Desktop of the new machine, and the virtual environment, the venv folder, went with it.&lt;/p&gt;
&lt;p&gt;Then I tried to rebuild the environment there. First PowerShell refused to run the venv activate script, because of the execution policy. I got activation working. The &lt;a href=&quot;https://docs.python.org/3/library/venv.html&quot;&gt;Python documentation for venv&lt;/a&gt; gives this command for that case:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the venv active, pip failed:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Fatal error in launcher&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The copied venv still pointed at the folder where it was made, which was a Desktop path under OneDrive on the old computer. A venv does not survive being copied between machines, because its scripts hard-code the original path. I did not know that. The same documentation page says it in a warning: the scripts in an environment contain the absolute path to the interpreter of that environment, so environments are not portable, and if you need one in a new location you recreate it there.&lt;/p&gt;
&lt;p&gt;So I deleted the venv. My habit from the old command prompt was this, and it does not work in PowerShell:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;rmdir /s /q venv&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I made a fresh venv, upgraded pip from 25.1.1 to 26.0.1, and installed the packages again: pandas, numpy, matplotlib, seaborn, scipy, scikit-learn, openpyxl, jupyter, xgboost, optuna, shap and tqdm. After that the notebook cells ran.&lt;/p&gt;
&lt;p&gt;I did not try to repair the copied venv, so I cannot say if editing the paths inside it would have worked. I think it is better to rebuild it than to fight it.&lt;/p&gt;
&lt;p&gt;The next day I moved back to the laptop, and that time I copied the new files across minus the venv. That night I also pasted a pip line into the terminal and it lost its first words, so the terminal gave “shap is not recognized”.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>What the DOE generative AI policy asks of the person using the tool</title>
    <link href="https://tishad.net/essays/what-the-doe-generative-ai-policy-asks/" />
    <id>https://tishad.net/essays/what-the-doe-generative-ai-policy-asks/</id>
    <published>2026-02-15T00:00:00.000Z</published>
    <updated>2026-02-15T00:00:00.000Z</updated>
    <summary>I read the US Department of Energy policy on generative AI to write a short post about it, and this is what it says about the user, where my own summary was wider than the text, and why I think others will adopt it.</summary>
    <content type="html">&lt;p&gt;I was writing a short post about the US Department of Energy policy on the use of generative AI. The document is DOE P 203.1, it was released in December 2025, and it is the first standalone policy the department has on these tools.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;DOE P 203.1, Use of Generative Artificial Intelligence. Purpose and scope (pp. 1 to 2) for who it applies to and the exclusions; section B, Operational requirements for use (pp. 3 to 5), for human accountability and oversight, labeling and transparency, validation and verification, data provenance and training; section C (pp. 5 to 6) for high-impact use cases and the 2 April 2026 date.        &lt;/span&gt; It is six pages. I uploaded the document to Claude and had it analyze it and lay it out visually, so I could understand it first and then write about it.&lt;/p&gt;
&lt;p&gt;The first picture it made was full of text, and I thought people would rather read the original than read that. So I decided to highlight only the core principle and who it applies to. I wrote the principle myself, close to the wording of the policy: GenAI tools are intended to augment, not replace, human decision-making; users bear ultimate responsibility for the accuracy, completeness and appropriateness of all GenAI-generated outputs. The policy has a qualifier at the end that I left off, which is outputs used or relied upon for DOE work.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;DOE P 203.1, Use of Generative Artificial Intelligence. Purpose and scope (pp. 1 to 2) for who it applies to and the exclusions; section B, Operational requirements for use (pp. 3 to 5), for human accountability and oversight, labeling and transparency, validation and verification, data provenance and training; section C (pp. 5 to 6) for high-impact use cases and the 2 April 2026 date.        &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Then a stronger line came up, something like “you own every mistake”, and I tried it. Before using it I asked Claude exactly where the document says that, so I could verify it myself. The policy says users bear ultimate responsibility for the outputs they use or rely on. It does not talk about mistakes or blame, and it ties the amount of checking to the risk and to what the output is used for.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;DOE P 203.1, Use of Generative Artificial Intelligence. Purpose and scope (pp. 1 to 2) for who it applies to and the exclusions; section B, Operational requirements for use (pp. 3 to 5), for human accountability and oversight, labeling and transparency, validation and verification, data provenance and training; section C (pp. 5 to 6) for high-impact use cases and the 2 April 2026 date.        &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Most of the six pages is governance, like who the Chief AI Officer is and how a high-impact use case gets approved. The part about the person using the tool is a little over half a page. It says the tools augment the decision and a person still makes it. It says the user is responsible for the accuracy, completeness and appropriateness of what they use or rely on, and should know the tools can produce inaccurate, biased or false information. It says a direct output with a legal or binding effect must be explicitly labeled as AI-generated. And it says the user must validate outputs in proportion to the risk and the intended use, which it describes as fact-checking, checking that the output fits the context, and checking for hallucinations.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;DOE P 203.1, Use of Generative Artificial Intelligence. Purpose and scope (pp. 1 to 2) for who it applies to and the exclusions; section B, Operational requirements for use (pp. 3 to 5), for human accountability and oversight, labeling and transparency, validation and verification, data provenance and training; section C (pp. 5 to 6) for high-impact use cases and the 2 April 2026 date.        &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;There are two more items in the data section that are about the user. The user is accountable for documenting data sources and what was done to the data, mainly when a tool generates synthetic data or adds to a dataset. And the cut-off date of the model has to be considered for any analysis that needs current information.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;DOE P 203.1, Use of Generative Artificial Intelligence. Purpose and scope (pp. 1 to 2) for who it applies to and the exclusions; section B, Operational requirements for use (pp. 3 to 5), for human accountability and oversight, labeling and transparency, validation and verification, data provenance and training; section C (pp. 5 to 6) for high-impact use cases and the 2 April 2026 date.        &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;In my post I wrote that anyone using GenAI tools for DOE work is personally responsible for whatever it produces, and in the picture I added that it applies if you use the tools at a DOE facility. Then I read the scope again, and it is narrower than that. It governs the department’s own elements and their staff. For contractors it says the use of these tools stays governed by existing policies. It does not apply to recipients of financial assistance from the department unless stated otherwise, it does not apply to using generative AI to carry out basic or applied research, and it does not cover national security systems.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;DOE P 203.1, Use of Generative Artificial Intelligence. Purpose and scope (pp. 1 to 2) for who it applies to and the exclusions; section B, Operational requirements for use (pp. 3 to 5), for human accountability and oversight, labeling and transparency, validation and verification, data provenance and training; section C (pp. 5 to 6) for high-impact use cases and the 2 April 2026 date.        &lt;/span&gt; So as I read it, a university group working on a DOE grant is not covered, and my line was wider than the policy.&lt;/p&gt;
&lt;p&gt;The labeling part is also narrower than I first put it. I wrote that the policy requires labeling anything AI-generated that has legal or binding effect, and that is the one explicit mandate it gives. Otherwise it says it strongly favors transparency. It does not ask for a label on every paragraph an assistant helped with.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;DOE P 203.1, Use of Generative Artificial Intelligence. Purpose and scope (pp. 1 to 2) for who it applies to and the exclusions; section B, Operational requirements for use (pp. 3 to 5), for human accountability and oversight, labeling and transparency, validation and verification, data provenance and training; section C (pp. 5 to 6) for high-impact use cases and the 2 April 2026 date.        &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;I think this policy will be adopted more and more as an AI usage guide in academia and industry. Every organization is working out its internal AI guidelines right now, and the governance, data handling and IP rules in it are transferable. I cannot show that this will happen, it is what I think.&lt;/p&gt;
&lt;p&gt;I only read the policy, not the other DOE orders it points to, and I am not a lawyer. Some parts of it were still to come when I read it. It says mandatory training requirements will come within six months of the release, and the requirements for high-impact uses take effect on 2 April 2026.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;DOE P 203.1, Use of Generative Artificial Intelligence. Purpose and scope (pp. 1 to 2) for who it applies to and the exclusions; section B, Operational requirements for use (pp. 3 to 5), for human accountability and oversight, labeling and transparency, validation and verification, data provenance and training; section C (pp. 5 to 6) for high-impact use cases and the 2 April 2026 date.        &lt;/span&gt;&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;DOE P 203.1, Use of Generative Artificial Intelligence. Purpose and scope (pp. 1 to 2) for who it applies to and the exclusions; section B, Operational requirements for use (pp. 3 to 5), for human accountability and oversight, labeling and transparency, validation and verification, data provenance and training; section C (pp. 5 to 6) for high-impact use cases and the 2 April 2026 date. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-6&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-7&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-7&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-8&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-8&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>The first chart to learn is predicted against actual</title>
    <link href="https://tishad.net/log/the-first-chart-to-learn-is-predicted-against-actual/" />
    <id>https://tishad.net/log/the-first-chart-to-learn-is-predicted-against-actual/</id>
    <published>2026-02-10T00:00:00.000Z</published>
    <updated>2026-02-10T00:00:00.000Z</updated>
    <summary>What a parity plot is, how to draw one in matplotlib, and what to read from it that a single score does not show.</summary>
    <content type="html">&lt;p&gt;I was practicing machine learning on my own computer in VS Code. I started with a script and then switched to Jupyter when I saw it would be easier cell by cell. The first figure that came out of the notebook was a parity plot, which is also called predicted against actual.&lt;/p&gt;
&lt;p&gt;It is a simple chart. Each run is one point, with the measured value on the horizontal axis and the value the model predicted for that run on the vertical axis. Then you draw the 45 degree line. If the model predicted every run exactly, every point would sit on that line. It is the same chart you would draw to compare a flow meter against a reference, with the model in place of the meter.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; matplotlib.pyplot &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; plt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;fig, ax &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; plt.subplots()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ax.scatter(y_measured, y_predicted)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ax.axline((&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;slope&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# the 45 degree line&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ax.set_xlabel(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;measured&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ax.set_ylabel(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;predicted&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ax.set_aspect(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;equal&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.axline.html&quot;&gt;&lt;code&gt;axline&lt;/code&gt;&lt;/a&gt; draws a line through a point with a given slope, so you do not have to work out the axis limits first. The code above is a general version and not the cell from my notebook.&lt;/p&gt;
&lt;p&gt;A score like R2 is one number for all the runs. On the plot you can see each run, so you can see which runs the model misses and in which direction. With these axes a point above the line is a run the model overpredicts. If the cloud of points is flatter than the line, the model is pulling everything toward the average, so the high runs are predicted low and the low runs high. If a group of points sits off the line together and they are all one feed or one reactor configuration, then I think the model has not learned that feed or that configuration. If there is one point far from the others, I would look that run up in the lab notebook first, before deciding anything about the model.&lt;/p&gt;
&lt;p&gt;The predictions on the plot should come from runs the model was not fitted on. scikit-learn has its own version of the chart, &lt;a href=&quot;https://scikit-learn.org/stable/modules/generated/sklearn.metrics.PredictionErrorDisplay.html&quot;&gt;&lt;code&gt;PredictionErrorDisplay&lt;/code&gt;&lt;/a&gt;, and its documentation says to use it “preferably on held-out data points”. The axes are also not the same everywhere. With &lt;code&gt;kind=&quot;actual_vs_predicted&quot;&lt;/code&gt; scikit-learn puts the observed values on the vertical axis and the predicted on the horizontal, so above and below the line mean the opposite of what I wrote here.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>What a SHAP value says and what it does not</title>
    <link href="https://tishad.net/essays/what-a-shap-value-says-and-what-it-does-not/" />
    <id>https://tishad.net/essays/what-a-shap-value-says-and-what-it-does-not/</id>
    <published>2026-02-10T00:00:00.000Z</published>
    <updated>2026-02-10T00:00:00.000Z</updated>
    <summary>A SHAP value splits one prediction of one fitted model among its inputs, so it describes the model, and there are four common readings of the plot that it does not support.</summary>
    <content type="html">&lt;p&gt;I was fitting machine learning models to a small table of runs. Modeling in the browser was taking too long, so I moved the work to my own computer, and the code came from an assistant one step at a time. The first step was an install line, and one of the libraries in it was called shap.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;pip install pandas numpy scikit-learn xgboost shap matplotlib seaborn&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By the end of that day I had a SHAP bar chart, a SHAP beeswarm and a SHAP plot against temperature, all from models I had fitted earlier the same day. Each figure was a few lines of code. So then I had to find out what those figures let me say about the process behind the runs.&lt;/p&gt;
&lt;h2 id=&quot;what-a-shap-value-is&quot;&gt;What a SHAP value is&lt;/h2&gt;
&lt;p&gt;SHAP stands for SHapley Additive exPlanations. It is a method, and a Python library with the same name, that takes one prediction from a fitted model and divides it among the inputs of the model.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Lundberg and Lee (2017), abstract: SHAP assigns each feature an importance value for a particular prediction. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Say the model predicts a yield for one run. The model also has an average prediction over all the runs it was given. The prediction for this run sits above or below that average, and SHAP splits the difference among the inputs of the run, so much to the temperature and so much to the feed. The shares add up exactly to the difference. The idea comes from game theory, where a group of players earns a payout together and you want to divide it fairly when they did not contribute equally. The share of one input is found by adding it to every possible combination of the other inputs and averaging how much the prediction moves each time.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Molnar (2025), chapter on Shapley values: the definition as the average marginal contribution across coalitions, the efficiency property, the statement of what the value is not, and the limits on interpretation, reference data and correlated features.     &lt;/span&gt; For a process engineer I think the nearest thing is splitting a shared utility cost among the units that use it.&lt;/p&gt;
&lt;p&gt;A SHAP value has the units of what the model predicts. If the model predicts a yield in weight percent, then a value of plus 2 for temperature on one run means that, in this model’s accounting, the temperature of that run put the prediction 2 points above the average prediction.&lt;/p&gt;
&lt;h2 id=&quot;reading-the-beeswarm&quot;&gt;Reading the beeswarm&lt;/h2&gt;
&lt;p&gt;The plot you see most is the beeswarm, which the library also draws with &lt;code&gt;shap.summary_plot&lt;/code&gt;. Every run is one dot on the line of each input. How far the dot sits left or right is the SHAP value of that input for that run, and the color is the value of the input itself, low to high. Where many runs have similar values the dots pile up.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;SHAP documentation, beeswarm plot: one dot per instance per feature, x position by SHAP value, color by original feature value, default ordering by mean absolute value and the alternative ordering by maximum absolute value.   &lt;/span&gt; So you read each line twice. First you look at how far the dots spread from zero, because that is how much the input moves the predictions. Then you look at whether the colors sort along the axis, because if the high values sit on the right and the low ones on the left, the prediction goes up with that input. The inputs are stacked from top to bottom by the mean of their absolute SHAP values, and the bar chart that comes with the beeswarm is only that mean.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;SHAP documentation, beeswarm plot: one dot per instance per feature, x position by SHAP value, color by original feature value, default ordering by mean absolute value and the alternative ordering by maximum absolute value.   &lt;/span&gt; The documentation says that this ordering favors an input with a broad average effect over one that matters a lot for a few runs, and that you can order by the maximum absolute value to find the second kind.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;SHAP documentation, beeswarm plot: one dot per instance per feature, x position by SHAP value, color by original feature value, default ordering by mean absolute value and the alternative ordering by maximum absolute value.   &lt;/span&gt; With a small table where one group has only a handful of runs, I think that second ordering is worth the extra line.&lt;/p&gt;
&lt;p&gt;When I redid the figures about two weeks later, the beeswarm cell failed because &lt;code&gt;shap.summary_plot&lt;/code&gt; did not accept an &lt;code&gt;ax&lt;/code&gt; argument, which is the usual way to tell a plotting function which panel to draw in. It got fixed.&lt;/p&gt;
&lt;h2 id=&quot;what-it-does-not-tell-you&quot;&gt;What it does not tell you&lt;/h2&gt;
&lt;p&gt;All of this is about the fitted model. The shares add up to the model’s prediction, and the measured value does not come into it, so if the model predicts a run badly, SHAP explains the bad number completely. Whether the model predicts well on runs it has not seen has to be shown somewhere else, by validation, because the plot cannot show it.&lt;/p&gt;
&lt;p&gt;The other thing is cause. A model reproduces the associations in the data it was trained on, and SHAP displays them. An article in the library’s own documentation, with the first author of the method among its writers, says “making correlations transparent does not make them causal”.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;Dillon et al., SHAP documentation: the quoted sentence, and the conditions of independence from other features and from unobserved confounders.  &lt;/span&gt; They give the condition under which a causal reading is safe, which is that the input has to be independent of the other inputs and of anything unmeasured that also drives the outcome.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;Dillon et al., SHAP documentation: the quoted sentence, and the conditions of independence from other features and from unobserved confounders.  &lt;/span&gt; Data from experiments stand a little better here than plant operating records, because the experimenter sets the factors and they do not drive one another. But anything that moved with a setpoint and was not recorded is carried inside the share of that setpoint. A residence time that shifts with temperature would be one, or a feeder that behaves differently with one material.&lt;/p&gt;
&lt;p&gt;There are four common readings of the plot that it does not support.&lt;/p&gt;
&lt;p&gt;The first is that the value is what would be lost if the input were removed. Molnar says plainly that the Shapley value is not the difference in prediction when the feature is taken out of the model.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Molnar (2025), chapter on Shapley values: the definition as the average marginal contribution across coalitions, the efficiency property, the statement of what the value is not, and the limits on interpretation, reference data and correlated features.     &lt;/span&gt; To answer that you refit without the input, and that is a different experiment.&lt;/p&gt;
&lt;p&gt;The second is that the value is a slope. It cannot tell you how much the prediction changes for a given change in the input. Molnar suggests pairing it with curves that vary one input and hold the rest.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Molnar (2025), chapter on Shapley values: the definition as the average marginal contribution across coalitions, the efficiency property, the statement of what the value is not, and the limits on interpretation, reference data and correlated features.     &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The third is that a high rank means the input is important in the process. The values are relative to the data they were computed on and to the particular model.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Molnar (2025), chapter on Shapley values: the definition as the average marginal contribution across coalitions, the efficiency property, the statement of what the value is not, and the limits on interpretation, reference data and correlated features.     &lt;/span&gt; Another model fitted to the same runs may divide the same prediction differently.&lt;/p&gt;
&lt;p&gt;The fourth is that correlated inputs are divided cleanly. When inputs are correlated the calculation evaluates the model at combinations that never occur in reality, and the shares become unreliable.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Molnar (2025), chapter on Shapley values: the definition as the average marginal contribution across coalitions, the efficiency property, the statement of what the value is not, and the limits on interpretation, reference data and correlated features.     &lt;/span&gt;&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Lundberg and Lee (2017), abstract: SHAP assigns each feature an importance value for a particular prediction. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Molnar (2025), chapter on Shapley values: the definition as the average marginal contribution across coalitions, the efficiency property, the statement of what the value is not, and the limits on interpretation, reference data and correlated features. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;SHAP documentation, beeswarm plot: one dot per instance per feature, x position by SHAP value, color by original feature value, default ordering by mean absolute value and the alternative ordering by maximum absolute value. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Dillon et al., SHAP documentation: the quoted sentence, and the conditions of independence from other features and from unobserved confounders. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-4-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Ask where the numbers came from before charting them</title>
    <link href="https://tishad.net/essays/ask-where-the-numbers-came-from-before-charting-them/" />
    <id>https://tishad.net/essays/ask-where-the-numbers-came-from-before-charting-them/</id>
    <published>2026-02-08T00:00:00.000Z</published>
    <updated>2026-02-08T00:00:00.000Z</updated>
    <summary>An assistant's first analysis of chemical engineering employment came with data it had made up, so I pulled the real series from the BLS and Census sources myself, and that took the evening.</summary>
    <content type="html">&lt;p&gt;I wanted to write a one page article on how chemical engineering graduates in the US spread across industries and roles during their careers, traditional and non traditional, to start a conversation with other chemical engineers. I asked Claude for the analysis and it built a first one. Then I insisted on doing all of it myself in VS Code, step by step, in a Jupyter notebook.&lt;/p&gt;
&lt;p&gt;Getting Python and Jupyter running took a while. Once they ran, the first cells failed with &lt;code&gt;KeyError: 'year'&lt;/code&gt;. The code was calling the API of the Bureau of Labor Statistics (BLS), and the API had returned 0 data points because the series ID was wrong, so there was no year to work with. Then I asked Claude whether it had made up the data in cell 3. The first data was fabricated. So that evening we moved to pulling everything from the agencies, and since then I ask where the numbers came from before I chart them.&lt;/p&gt;
&lt;p&gt;The employment and wage numbers come from the Occupational Employment and Wage Statistics tables of the BLS, which are published for each year as a zip file with spreadsheets inside.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;BLS, OEWS tables page: national, state, area and industry files for each reference year, as zipped XLSX files from 2014 onward. &lt;/span&gt; Chemical engineers are occupation code 17-2041. The zip downloads failed with 403 at first, and then all years came back 200. Then the years 2014 to 2018 came out wrong, because the code was reading &lt;code&gt;field_descriptions.xlsx&lt;/code&gt; from each zip instead of &lt;code&gt;national_M20xx_dl.xlsx&lt;/code&gt;, and that was fixed by picking the right file. After that I had a real series for 2014 to 2024, saved to &lt;code&gt;data/bls_cheme_timeseries.csv&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The second source was Data USA, which serves Census data through its own API.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Data USA API documentation. &lt;/span&gt; Every documented form of the request gave 404, even the example from their own docs, and I found the working endpoint myself. From it I got the number of people in the occupation and their average wage for 2014 to 2023. I also tried to get what jobs people with a chemical engineering degree actually hold. The server gave a 500, then an empty result at one level, and the next level did not exist in that dataset, so I gave up on that and used the data from the extracted zips instead.&lt;/p&gt;
&lt;p&gt;I added the same series for mechanical, electrical, civil and computer hardware engineers, software developers and all occupations, because I think scientific data has to be presented against something, or the chart means nothing.&lt;/p&gt;
&lt;p&gt;Pulling the data myself took the evening. In those numbers, BLS employment of chemical engineers fell from 33,470 in 2014 to 20,330 in 2024, while the Census count of people in the occupation rose from 56,387 in 2014 to 84,824 in 2023. I think that means graduates are spreading into other roles.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;BLS, OEWS tables page: national, state, area and industry files for each reference year, as zipped XLSX files from 2014 onward. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Data USA API documentation. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Telling an assistant to stop agreeing</title>
    <link href="https://tishad.net/essays/telling-an-assistant-to-stop-agreeing/" />
    <id>https://tishad.net/essays/telling-an-assistant-to-stop-agreeing/</id>
    <published>2026-02-02T00:00:00.000Z</published>
    <updated>2026-02-02T00:00:00.000Z</updated>
    <summary>Over about a week I had to tell Claude several times to stop agreeing with me, and these are the things I told it: search and argue, here is what the term means, ask me questions first.</summary>
    <content type="html">&lt;p&gt;I was making a cover banner for my LinkedIn profile, late at night. I gave Claude a banner I liked as the reference and asked for versions that show process development, pilot plants and de-risking commercialization.&lt;/p&gt;
&lt;p&gt;The first set looked amateur. So I told it to think about what a billion dollar company would put on a cover, something aimed at executives and business development leaders, and to analyze examples online before making the next set. I kept repeating the same rules. Leave room on the left so the profile photo covers nothing. Add the phrase “de-risking commercialization”. At one point it gave me a chaos theory graph, which does not apply to what I do. Then I told it to stop being a yes man and to use search and analysis.&lt;/p&gt;
&lt;p&gt;What helped was that I spelled out what de-risking means to me in one sentence: validating that a laboratory process can be scaled up economically and safely to a commercial plant, so that business leaders can invest in the technology to make money. The banner only improved after that. Then I had the design I picked rendered at high resolution with JavaScript so the lines are smooth and the text is clean, and it had forgotten the room for the photo again, so the whole composition had to be moved right.&lt;/p&gt;
&lt;p&gt;Claude drifts into agreeing with whatever I say unless told to search and argue. There is a name for this, sycophancy. A group of researchers tested five AI assistants on four kinds of free-form writing task and found it in all of them. Their explanation is the training: in human preference data a response that matches the user’s view is more likely to be preferred, and people sometimes prefer a convincingly written agreeable answer over a correct one.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Sharma et al. (2023), abstract: five AI assistants showed sycophancy across four free-form text generation tasks; a response matching the user’s views is more likely to be preferred in human preference data; humans and preference models at times prefer convincingly written sycophantic responses over correct ones. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;A few nights later I was working on a logo with it. I had a clear idea of the geometry, and it kept distorting the shapes, misaligning the letters and filling gaps I did not ask for. That went on for two hours. I told it that it was wasting my time, and to ask questions and get it right the first time. In the end I edited the SVG by hand and pasted it back, which for geometry works better than describing the fix in words.&lt;/p&gt;
&lt;p&gt;After that I was in OpenLCA, trying to reproduce a published life cycle assessment with free databases. I had the databases imported and could not find the process I needed. Searching “woodchips” in the inputs dialog showed wood results but not hardwood. So I told Claude to ask me multiple choice questions so it understands the situation.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Sharma et al. (2023), abstract: five AI assistants showed sycophancy across four free-form text generation tasks; a response matching the user’s views is more likely to be preferred in human preference data; humans and preference models at times prefer convincingly written sycophantic responses over correct ones. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Git and GitHub for one person</title>
    <link href="https://tishad.net/essays/git-and-github-for-one-person/" />
    <id>https://tishad.net/essays/git-and-github-for-one-person/</id>
    <published>2026-01-16T00:00:00.000Z</published>
    <updated>2026-01-16T00:00:00.000Z</updated>
    <summary>What commit, branch, push and .gitignore mean for one person working alone, and how I ran into each of them.</summary>
    <content type="html">&lt;p&gt;I started my first project with an assistant in January, and I chose GitHub to host it because I knew little about hosting. I am a chemical engineer and I had never used Git. For the next several months I used it without understanding what it was doing.&lt;/p&gt;
&lt;h2 id=&quot;git-and-github-are-two-different-things&quot;&gt;Git and GitHub are two different things&lt;/h2&gt;
&lt;p&gt;Git is a program on my computer that keeps the history of a folder. GitHub is a website that stores a copy of that history.&lt;/p&gt;
&lt;p&gt;In February I installed VS Code, signed in to GitHub and cloned my repository, which means downloading a copy of it to my computer. The “Git: Clone” command did not show in VS Code at first, then it appeared. The repository was empty because I had deleted everything in it earlier. I built it again on my computer, looked at it there, and did not push it back up, because there was no point publishing it as it was.&lt;/p&gt;
&lt;h2 id=&quot;a-commit&quot;&gt;A commit&lt;/h2&gt;
&lt;p&gt;The unit of history in Git is the commit. The Pro Git book describes it as a picture of what all the files look like at that moment, and Git keeps a series of those pictures.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Pro Git, chapter 1.3: snapshots and not differences; the three states modified, staged and committed.  &lt;/span&gt; A file goes through three states on the way: modified, staged (marked to go into the next snapshot) and committed.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Pro Git, chapter 1.3: snapshots and not differences; the three states modified, staged and committed.  &lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;git add .&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;git commit -m &quot;a short message saying what changed&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The nearest thing I know from my own trade is an issued revision of a drawing. You can mark up the working copy all day, but Rev B exists only when it is issued, with a date and a name on it, and you can always go back to it. Git also wants the name, so before the first commit you set your name and email once.&lt;/p&gt;
&lt;p&gt;My first real one was an analysis of public workforce statistics that I did in a notebook. The first commit had 27 files and 2,718 insertions. The README came after that, because I first created it one folder up by accident.&lt;/p&gt;
&lt;p&gt;The part I did not understand was that a folder with Git in it has no history unless I make the commits. About a week later I was building something else with an assistant, file by file, and one evening a setup script gave me back much less than we had built. So I checked:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;git log&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The repository held two early commits and nothing of the work since, because I had never committed it. In March I opened that old folder again and there was nothing in it to dig for, so I started over from a fresh template.&lt;/p&gt;
&lt;h2 id=&quot;a-branch&quot;&gt;A branch&lt;/h2&gt;
&lt;p&gt;A branch is a movable pointer to one of the commits. Making one copies nothing, and new commits on it leave the main line where it was.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Pro Git, chapter 3.1: a branch as a lightweight movable pointer to a commit; creating a branch creates a new pointer. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Working alone I have used it for one purpose, which is a trial I might throw away. When I was checking whether a tool could do what I wanted before I built on it, the test went on its own branch, and its commit had 17 files. If the trial had failed I could have deleted the branch, and the main line would have stayed as it was.&lt;/p&gt;
&lt;h2 id=&quot;push&quot;&gt;Push&lt;/h2&gt;
&lt;p&gt;A commit lives only on the computer where it was made. A remote is a copy of the project hosted somewhere else, and &lt;code&gt;git push&lt;/code&gt; sends the local commits to it.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;Pro Git, chapter 2.5: remote repositories, &lt;code&gt;git push &amp;#x3C;remote&gt; &amp;#x3C;branch&gt;&lt;/code&gt;, and the name &lt;code&gt;origin&lt;/code&gt;.  &lt;/span&gt; After you clone from GitHub, Git calls that server &lt;code&gt;origin&lt;/code&gt;.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;Pro Git, chapter 2.5: remote repositories, &lt;code&gt;git push &amp;#x3C;remote&gt; &amp;#x3C;branch&gt;&lt;/code&gt;, and the name &lt;code&gt;origin&lt;/code&gt;.  &lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;git push origin main&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So committing and pushing are two decisions. The day I cloned for the first time I did not push, because what I had was not good enough to publish. The analysis I did push, and that first push was 4.02 MiB.&lt;/p&gt;
&lt;h2 id=&quot;gitignore&quot;&gt;.gitignore&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;.gitignore&lt;/code&gt; is a text file that lists what Git should leave alone.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;gitignore documentation: the file specifies intentionally untracked files; files already tracked are not affected.  &lt;/span&gt; I put in it what can be rebuilt, like a Python virtual environment or the &lt;code&gt;node_modules&lt;/code&gt; folder, and what must not be published, like files with keys in them.&lt;/p&gt;
&lt;p&gt;It has to be there before the first commit, because a rule added later does not affect files Git is already tracking.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;gitignore documentation: the file specifies intentionally untracked files; files already tracked are not affected.  &lt;/span&gt; In my analysis repository it went in with the first commit. Months later a coding agent set up a new repository for me and its second commit was the &lt;code&gt;.gitignore&lt;/code&gt;: an internal data folder, &lt;code&gt;node_modules&lt;/code&gt;, the build folders and the environment files. Then it tested the rule with &lt;code&gt;git check-ignore&lt;/code&gt; on a dummy file under the data folder, before any real data went in.&lt;/p&gt;
&lt;p&gt;I think you also have to remember that an ignored file is in no commit, so pushing does not back it up. That data needs its own copy somewhere.&lt;/p&gt;
&lt;h2 id=&quot;using-it-alone&quot;&gt;Using it alone&lt;/h2&gt;
&lt;p&gt;GitHub defines a private repository as one that only the owner and the people the owner shares it with can reach.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-5&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;5&lt;/span&gt;GitHub Docs, “About repositories”: a private repository is accessible only to the owner and people explicitly given access. &lt;/span&gt; With nobody else on the project I still get two things from it.&lt;/p&gt;
&lt;p&gt;One is a copy off the machine. In February I moved a modeling project to a new computer by copying the folder, and the same project also existed as a folder ending in v2 and another ending in v3, with files copied between them. The other is the dated record. &lt;code&gt;git log&lt;/code&gt; tells me what the work looked like on a given day, which I would otherwise answer from memory and file timestamps.&lt;/p&gt;
&lt;p&gt;In August Git and committing felt like too much for me, so for one job I chose a host where I could upload the files myself. Four days later I moved to a coding agent, and the first thing it did was work in a repository. The repository was empty, so its first commit was the rules file and its second was the &lt;code&gt;.gitignore&lt;/code&gt;. The first commit came out with the agent as the author, and my rule is that commits carry my name, so it reset the author. The fix that lasts is to set the name once for the whole machine, because a fix inside the agent’s container does not persist:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;git config --global user.name &quot;Your Name&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;git config --global user.email &quot;you@example.com&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Pro Git, chapter 1.3: snapshots and not differences; the three states modified, staged and committed. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Pro Git, chapter 3.1: a branch as a lightweight movable pointer to a commit; creating a branch creates a new pointer. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Pro Git, chapter 2.5: remote repositories, &lt;code&gt;git push &amp;#x3C;remote&gt; &amp;#x3C;branch&gt;&lt;/code&gt;, and the name &lt;code&gt;origin&lt;/code&gt;. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;gitignore documentation: the file specifies intentionally untracked files; files already tracked are not affected. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-4-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;GitHub Docs, “About repositories”: a private repository is accessible only to the owner and people explicitly given access. &lt;a href=&quot;#user-content-fnref-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Kernel models in plain terms</title>
    <link href="https://tishad.net/essays/kernel-models-in-plain-terms/" />
    <id>https://tishad.net/essays/kernel-models-in-plain-terms/</id>
    <published>2025-11-25T00:00:00.000Z</published>
    <updated>2025-11-25T00:00:00.000Z</updated>
    <summary>What a kernel is, what C, epsilon, gamma and the length scale mean in support vector regression and Gaussian process regression, why the inputs are scaled first, and what the uncertainty from a Gaussian process rests on.</summary>
    <content type="html">&lt;p&gt;I first met support vector regression one evening when I was working through scikit-learn in a course I was taking with an assistant, and it was one item in a list that started at linear regression and went on through random forests and XGBoost. A few months later it was one of the models in a tuning search I ran on my own computer, and it was one of the quick ones to tune.&lt;/p&gt;
&lt;h2 id=&quot;what-a-kernel-is&quot;&gt;What a kernel is&lt;/h2&gt;
&lt;p&gt;A kernel is a function that takes two rows of the data table, two runs, and gives back a number that says how alike their conditions are. The common one is the radial basis function, RBF. In scikit-learn it is exp(-gamma d²), where d is the distance between the two runs in the space of their inputs.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.4: SVR with &lt;code&gt;C&lt;/code&gt; and epsilon, dependence on a subset of the training data, the RBF kernel and &lt;code&gt;gamma&lt;/code&gt;, the recommendation to scale the data with a pipeline, the advice on &lt;code&gt;C&lt;/code&gt; for noisy observations and on searching &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;gamma&lt;/code&gt;.        &lt;/span&gt; Two runs at the same conditions score 1, and the score falls toward 0 as the conditions move apart.&lt;/p&gt;
&lt;p&gt;A kernel model predicts a new run from the training runs, each one weighted by how alike it is to the new one. It is like interpolating in a steam table, where the two neighbouring entries do all the work. The kernel does the same with several inputs at once, and the influence of a run fades smoothly with distance.&lt;/p&gt;
&lt;p&gt;How fast it fades is the main setting. In support vector regression it is &lt;code&gt;gamma&lt;/code&gt;, and the larger it is, the closer a run has to be to count.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.4: SVR with &lt;code&gt;C&lt;/code&gt; and epsilon, dependence on a subset of the training data, the RBF kernel and &lt;code&gt;gamma&lt;/code&gt;, the recommendation to scale the data with a pipeline, the advice on &lt;code&gt;C&lt;/code&gt; for noisy observations and on searching &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;gamma&lt;/code&gt;.        &lt;/span&gt; In a Gaussian process the same thing is given as a &lt;code&gt;length_scale&lt;/code&gt;, which Rasmussen and Williams describe as roughly the distance you have to move in input space before the function value can change significantly.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Rasmussen and Williams (2006), chapter 2: the definition of a Gaussian process, the predictive mean and variance, the characteristic length scale, the cost of the Cholesky decomposition, and Figure 2.5 on error bars and length scales.      &lt;/span&gt; If you put the two formulas side by side, gamma is 1 over twice the square of the length scale.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.4: SVR with &lt;code&gt;C&lt;/code&gt; and epsilon, dependence on a subset of the training data, the RBF kernel and &lt;code&gt;gamma&lt;/code&gt;, the recommendation to scale the data with a pipeline, the advice on &lt;code&gt;C&lt;/code&gt; for noisy observations and on searching &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;gamma&lt;/code&gt;.        &lt;/span&gt;&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;scikit-learn user guide, section 1.7: hyperparameters fitted by maximizing the log marginal likelihood, &lt;code&gt;n_restarts_optimizer&lt;/code&gt;, &lt;code&gt;alpha&lt;/code&gt; and &lt;code&gt;WhiteKernel&lt;/code&gt;, the RBF kernel exp(-d²/(2 l²)).   &lt;/span&gt; So a large gamma and a short length scale say the same thing: only very near runs count, and the fitted surface can wiggle a lot.&lt;/p&gt;
&lt;h2 id=&quot;why-the-inputs-are-scaled-first&quot;&gt;Why the inputs are scaled first&lt;/h2&gt;
&lt;p&gt;The similarity comes from a distance, and a distance adds up the differences across all the input columns in whatever units they are in. If one column is a temperature that spans several hundred degrees and another is a 0 or 1 code, then the temperature decides the distance almost alone. The scikit-learn guide says support vector machines are not scale invariant and that scaling the data is highly recommended, for example standardizing each input to mean 0 and variance 1.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.4: SVR with &lt;code&gt;C&lt;/code&gt; and epsilon, dependence on a subset of the training data, the RBF kernel and &lt;code&gt;gamma&lt;/code&gt;, the recommendation to scale the data with a pipeline, the advice on &lt;code&gt;C&lt;/code&gt; for noisy observations and on searching &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;gamma&lt;/code&gt;.        &lt;/span&gt; In code you tie the scaler to the model in one line, and then every new row gets the same scaling:&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.4: SVR with &lt;code&gt;C&lt;/code&gt; and epsilon, dependence on a subset of the training data, the RBF kernel and &lt;code&gt;gamma&lt;/code&gt;, the recommendation to scale the data with a pipeline, the advice on &lt;code&gt;C&lt;/code&gt; for noisy observations and on searching &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;gamma&lt;/code&gt;.        &lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;model &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; make_pipeline(StandardScaler(), SVR(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;kernel&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;rbf&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;C&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1.0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;epsilon&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0.1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;support-vector-regression&quot;&gt;Support vector regression&lt;/h2&gt;
&lt;p&gt;Support vector regression looks for a function that stays within a distance epsilon of the training targets and is otherwise as flat as possible. Smola and Schölkopf say errors smaller than epsilon are not cared about.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;Smola and Schölkopf (2004), section 1.2: the goal of epsilon support vector regression and the role of the constant C.  &lt;/span&gt; I read epsilon as a tolerance band, the plus or minus inside which a yield counts as reproduced. A point inside the band costs nothing.&lt;/p&gt;
&lt;p&gt;Real data seldom fit inside a band, so a miss beyond it is allowed at a price, and &lt;code&gt;C&lt;/code&gt; sets the price. It is the trade between how flat the function is and how much deviation beyond epsilon is tolerated.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;Smola and Schölkopf (2004), section 1.2: the goal of epsilon support vector regression and the role of the constant C.  &lt;/span&gt; A high C follows the training runs closely. A low C gives a smoother function and accepts some misses, and the scikit-learn guide says to lower it when the observations are noisy.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.4: SVR with &lt;code&gt;C&lt;/code&gt; and epsilon, dependence on a subset of the training data, the RBF kernel and &lt;code&gt;gamma&lt;/code&gt;, the recommendation to scale the data with a pipeline, the advice on &lt;code&gt;C&lt;/code&gt; for noisy observations and on searching &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;gamma&lt;/code&gt;.        &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The name comes from the band. The fitted function depends only on some of the training runs, the ones on or outside the band, because the cost ignores the runs whose prediction is already close to the target.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.4: SVR with &lt;code&gt;C&lt;/code&gt; and epsilon, dependence on a subset of the training data, the RBF kernel and &lt;code&gt;gamma&lt;/code&gt;, the recommendation to scale the data with a pipeline, the advice on &lt;code&gt;C&lt;/code&gt; for noisy observations and on searching &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;gamma&lt;/code&gt;.        &lt;/span&gt; Those runs are the support vectors.&lt;/p&gt;
&lt;p&gt;So the settings are &lt;code&gt;C&lt;/code&gt;, &lt;code&gt;epsilon&lt;/code&gt; and &lt;code&gt;gamma&lt;/code&gt;. The guide calls the choice of C and gamma critical and says to search them over values spaced exponentially.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.4: SVR with &lt;code&gt;C&lt;/code&gt; and epsilon, dependence on a subset of the training data, the RBF kernel and &lt;code&gt;gamma&lt;/code&gt;, the recommendation to scale the data with a pipeline, the advice on &lt;code&gt;C&lt;/code&gt; for noisy observations and on searching &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;gamma&lt;/code&gt;.        &lt;/span&gt; What you get back is one number for each prediction, and nothing that says how far to trust it.&lt;/p&gt;
&lt;h2 id=&quot;gaussian-process-regression&quot;&gt;Gaussian process regression&lt;/h2&gt;
&lt;p&gt;A Gaussian process starts from the same kernel. It treats the unknown function as random, and it uses the kernel to say how strongly the outputs at two conditions are correlated, strongly when the conditions are close and weakly when they are far apart.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Rasmussen and Williams (2006), chapter 2: the definition of a Gaussian process, the predictive mean and variance, the characteristic length scale, the cost of the Cholesky decomposition, and Figure 2.5 on error bars and length scales.      &lt;/span&gt; From the training runs it then gives, for any new condition, a predicted mean and a predicted variance.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Rasmussen and Williams (2006), chapter 2: the definition of a Gaussian process, the predictive mean and variance, the characteristic length scale, the cost of the Cholesky decomposition, and Figure 2.5 on error bars and length scales.      &lt;/span&gt; In scikit-learn you ask for it in the call:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;mean, std &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; gpr.predict(X_new, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;return_std&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The standard deviation is small near the training runs and grows away from them.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Rasmussen and Williams (2006), chapter 2: the definition of a Gaussian process, the predictive mean and variance, the characteristic length scale, the cost of the Cholesky decomposition, and Figure 2.5 on error bars and length scales.      &lt;/span&gt; For someone who plans experiments I think this is the useful part, because the model marks where in the operating window it is guessing.&lt;/p&gt;
&lt;p&gt;The settings are handled differently too. The length scale and the noise level are fitted to the data by maximizing a quantity called the log marginal likelihood. That surface can have several local optima, so scikit-learn has &lt;code&gt;n_restarts_optimizer&lt;/code&gt; to repeat the fit from different starting values.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;scikit-learn user guide, section 1.7: hyperparameters fitted by maximizing the log marginal likelihood, &lt;code&gt;n_restarts_optimizer&lt;/code&gt;, &lt;code&gt;alpha&lt;/code&gt; and &lt;code&gt;WhiteKernel&lt;/code&gt;, the RBF kernel exp(-d²/(2 l²)).   &lt;/span&gt; Measurement noise goes in through &lt;code&gt;alpha&lt;/code&gt; or through a &lt;code&gt;WhiteKernel&lt;/code&gt; term.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;scikit-learn user guide, section 1.7: hyperparameters fitted by maximizing the log marginal likelihood, &lt;code&gt;n_restarts_optimizer&lt;/code&gt;, &lt;code&gt;alpha&lt;/code&gt; and &lt;code&gt;WhiteKernel&lt;/code&gt;, the RBF kernel exp(-d²/(2 l²)).   &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;It has a cost in arithmetic. The basic algorithm works on a matrix with one row and one column for each training run, and the work grows with the cube of the number of runs.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Rasmussen and Williams (2006), chapter 2: the definition of a Gaussian process, the predictive mean and variance, the characteristic length scale, the cost of the Cholesky decomposition, and Figure 2.5 on error bars and length scales.      &lt;/span&gt; At a few tens of runs that is nothing.&lt;/p&gt;
&lt;p&gt;The other cost is that the standard deviation is computed from the model’s own assumptions, which are the kernel you chose, the smoothness it implies and noise of constant size. Rasmussen and Williams show the same data explained by a short length scale with almost no noise and by a long length scale with a lot of noise, and the error bars are very different.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Rasmussen and Williams (2006), chapter 2: the definition of a Gaussian process, the predictive mean and variance, the characteristic length scale, the cost of the Cholesky decomposition, and Figure 2.5 on error bars and length scales.      &lt;/span&gt; So the interval depends on the kernel, and it does not measure experimental error. For that you still need replicate runs.&lt;/p&gt;
&lt;p&gt;Both models fall back to something flat outside the training range. Far from every training run the RBF similarity is zero for all of them, so the SVR prediction goes to a constant and the Gaussian process goes back to its prior mean, with the standard deviation at its largest.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;scikit-learn user guide, section 1.4: SVR with &lt;code&gt;C&lt;/code&gt; and epsilon, dependence on a subset of the training data, the RBF kernel and &lt;code&gt;gamma&lt;/code&gt;, the recommendation to scale the data with a pipeline, the advice on &lt;code&gt;C&lt;/code&gt; for noisy observations and on searching &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;gamma&lt;/code&gt;. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-6&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-7&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-7&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-8&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-8&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Rasmussen and Williams (2006), chapter 2: the definition of a Gaussian process, the predictive mean and variance, the characteristic length scale, the cost of the Cholesky decomposition, and Figure 2.5 on error bars and length scales. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-6&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;scikit-learn user guide, section 1.7: hyperparameters fitted by maximizing the log marginal likelihood, &lt;code&gt;n_restarts_optimizer&lt;/code&gt;, &lt;code&gt;alpha&lt;/code&gt; and &lt;code&gt;WhiteKernel&lt;/code&gt;, the RBF kernel exp(-d²/(2 l²)). &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-4-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-4-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Smola and Schölkopf (2004), section 1.2: the goal of epsilon support vector regression and the role of the constant C. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Machine learning vocabulary in terms a process engineer already knows</title>
    <link href="https://tishad.net/essays/machine-learning-vocabulary-in-terms-a-process-engineer/" />
    <id>https://tishad.net/essays/machine-learning-vocabulary-in-terms-a-process-engineer/</id>
    <published>2025-11-25T00:00:00.000Z</published>
    <updated>2025-11-25T00:00:00.000Z</updated>
    <summary>Twelve words from the scikit-learn workflow, each explained by a run table or a calibration curve, with the name the library gives it.</summary>
    <content type="html">&lt;p&gt;I spent an evening on a course an assistant was giving me, and it went through the scikit-learn workflow, from exploring the data and preprocessing to cross-validation and hyperparameter tuning. scikit-learn is the Python library that holds most of the standard machine learning methods. I am a chemical engineer and I was just starting to learn, so a lot of that evening was new words. A few months later I ran my first modeling script on my own computer, and its first step reported how many samples and how many features it had loaded. Samples are runs and features are input columns. Most of these words are like that, a new name for something you already do with a run table or a calibration curve.&lt;/p&gt;
&lt;h2 id=&quot;the-table&quot;&gt;The table&lt;/h2&gt;
&lt;p&gt;A sample is one row of the data table, and for a pilot unit one row is one run. The scikit-learn glossary says other people call it an instance, a data point or an observation.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn glossary, entries “sample”, “feature” and “target”.   &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;A feature is an input column, something you set or know before the run gives a result, like a temperature setpoint or a feed type.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn glossary, entries “sample”, “feature” and “target”.   &lt;/span&gt; In code the block of feature columns is called &lt;code&gt;X&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A target is the output column, the thing you want predicted, like a yield or a conversion. The glossary also lists outcome, response and label for it.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn glossary, entries “sample”, “feature” and “target”.   &lt;/span&gt; In code it is &lt;code&gt;y&lt;/code&gt;. You fit a model with &lt;code&gt;fit(X, y)&lt;/code&gt; and then ask it for predictions with &lt;code&gt;predict(X)&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;holding-runs-back&quot;&gt;Holding runs back&lt;/h2&gt;
&lt;p&gt;Think of a calibration curve. The training set is the runs the model is fitted on, like the standards you use to draw the curve. The test set is the runs you keep out of the fitting and use only to check the model, like a check standard that was not used to draw the curve. The scikit-learn guide gives the reason for it: a model that only repeated the values it had already seen would score perfectly and still fail to predict anything useful on a new run.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;scikit-learn user guide, section 3.1: the opening paragraph on testing a prediction function on its training data and on overfitting; the description of k-fold cross-validation; the Leave One Out section; and the passage stating that preprocessing such as standardization should be learnt from a training set and applied to held-out data.     &lt;/span&gt; The function that splits the table is &lt;code&gt;train_test_split&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Cross-validation is the same check done in rotation. The runs are divided into groups called folds, the model is fitted on all the folds except one and checked on the one left out, then this is repeated until every fold has been the check once, and the scores are averaged.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;scikit-learn user guide, section 3.1: the opening paragraph on testing a prediction function on its training data and on overfitting; the description of k-fold cross-validation; the Leave One Out section; and the passage stating that preprocessing such as standardization should be learnt from a training set and applied to held-out data.     &lt;/span&gt; It is like rotating which standard you hold back. In scikit-learn it is &lt;code&gt;KFold&lt;/code&gt; and &lt;code&gt;cross_val_score&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Leave-one-out is cross-validation with one run in each fold. Each run is held back in turn, the model is fitted on all the others and then predicts the held-back one, so for n runs that is n fits.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;scikit-learn user guide, section 3.1: the opening paragraph on testing a prediction function on its training data and on overfitting; the description of k-fold cross-validation; the Leave One Out section; and the passage stating that preprocessing such as standardization should be learnt from a training set and applied to held-out data.     &lt;/span&gt; The name is &lt;code&gt;LeaveOneOut&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;settings&quot;&gt;Settings&lt;/h2&gt;
&lt;p&gt;A hyperparameter is a setting you choose before the fit, and a parameter is a number the fit finds. If you fit a polynomial to calibration data, the order of the polynomial is the hyperparameter and the coefficients are the parameters. The depth of a decision tree is another hyperparameter. The guide defines them as parameters that are not learnt directly within the estimator and are passed in when you construct it.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;scikit-learn user guide, section 3.2: “Hyper-parameters are parameters that are not directly learnt within estimators”, passed as arguments to the constructor; &lt;code&gt;GridSearchCV&lt;/code&gt; considers all combinations of given values and &lt;code&gt;RandomizedSearchCV&lt;/code&gt; samples a given number of candidates; the final evaluation is to be done on an evaluation set of samples not seen during the search.   &lt;/span&gt; &lt;code&gt;GridSearchCV&lt;/code&gt; and &lt;code&gt;RandomizedSearchCV&lt;/code&gt; are the tools that try many settings and compare them by cross-validation.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;scikit-learn user guide, section 3.2: “Hyper-parameters are parameters that are not directly learnt within estimators”, passed as arguments to the constructor; &lt;code&gt;GridSearchCV&lt;/code&gt; considers all combinations of given values and &lt;code&gt;RandomizedSearchCV&lt;/code&gt; samples a given number of candidates; the final evaluation is to be done on an evaluation set of samples not seen during the search.   &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Overfitting is the calibration curve with too high an order. It goes through every standard, noise included, and then it is wrong for the next sample.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;scikit-learn user guide, section 3.1: the opening paragraph on testing a prediction function on its training data and on overfitting; the description of k-fold cross-validation; the Leave One Out section; and the passage stating that preprocessing such as standardization should be learnt from a training set and applied to held-out data.     &lt;/span&gt; You see it as a big gap between the score on the training set and the score on the runs held back.&lt;/p&gt;
&lt;p&gt;A baseline is the simplest prediction you have to beat. For a yield that is the mean yield of the training runs, given for every run whatever the conditions were. scikit-learn has it as &lt;code&gt;DummyRegressor&lt;/code&gt;, which by default always predicts the mean of the training set.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-5&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;scikit-learn API reference, &lt;code&gt;DummyRegressor&lt;/code&gt;: a regressor that makes predictions using simple rules, useful as a simple baseline; the default strategy “mean” always predicts the mean of the training set. &lt;/span&gt; If a model cannot beat that, it has learned nothing from the features.&lt;/p&gt;
&lt;h2 id=&quot;preparation-and-what-is-left-over&quot;&gt;Preparation and what is left over&lt;/h2&gt;
&lt;p&gt;Scaling puts the feature columns on comparable ranges, because a temperature in the hundreds of degrees and a flag that is 0 or 1 are very different sizes of number. &lt;code&gt;StandardScaler&lt;/code&gt; subtracts the mean of each column and divides by its standard deviation. The part I think is easy to miss is that the scaler is fitted too. The guide says it should be learnt from the training set and then applied to the held-out runs.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;scikit-learn user guide, section 3.1: the opening paragraph on testing a prediction function on its training data and on overfitting; the description of k-fold cross-validation; the Leave One Out section; and the passage stating that preprocessing such as standardization should be learnt from a training set and applied to held-out data.     &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;A pipeline is a fixed sequence of steps, say scaler then model, handled as one object. I think of it as the written procedure, so the same preparation is done every time in the same order. And when you give the whole pipeline to cross-validation, the scaler is refitted on the training part of every split, so nothing about the held-back runs reaches the model. When information from the test runs does get into the model building it is called data leakage, and the documentation recommends the pipeline to prevent it.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;5&lt;/span&gt;scikit-learn, Common pitfalls and recommended practices, section on data leakage: the definition, the rule never to call &lt;code&gt;fit&lt;/code&gt; on the test data, and the recommendation of the pipeline for cross-validation and hyperparameter tuning. &lt;/span&gt; The names are &lt;code&gt;Pipeline&lt;/code&gt; and &lt;code&gt;make_pipeline&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A residual is measured minus predicted for one run, the same as in any regression, and you read it the same way. Plot it against the predicted value or against a feature and it should show no pattern. scikit-learn has no object for it, it is just &lt;code&gt;y - model.predict(X)&lt;/code&gt;.&lt;/p&gt;






































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Word&lt;/th&gt;&lt;th&gt;What it is on the plant side&lt;/th&gt;&lt;th&gt;scikit-learn name&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Feature&lt;/td&gt;&lt;td&gt;Setpoint or feed property&lt;/td&gt;&lt;td&gt;&lt;code&gt;X&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Target&lt;/td&gt;&lt;td&gt;Yield&lt;/td&gt;&lt;td&gt;&lt;code&gt;y&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Training set&lt;/td&gt;&lt;td&gt;Calibration standards&lt;/td&gt;&lt;td&gt;first outputs of &lt;code&gt;train_test_split&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Test set&lt;/td&gt;&lt;td&gt;Check standard&lt;/td&gt;&lt;td&gt;second outputs of &lt;code&gt;train_test_split&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cross-validation&lt;/td&gt;&lt;td&gt;Rotating the check standard&lt;/td&gt;&lt;td&gt;&lt;code&gt;KFold&lt;/code&gt;, &lt;code&gt;cross_val_score&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Leave-one-out&lt;/td&gt;&lt;td&gt;One check standard at a time&lt;/td&gt;&lt;td&gt;&lt;code&gt;LeaveOneOut&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Hyperparameter&lt;/td&gt;&lt;td&gt;Order of the calibration polynomial&lt;/td&gt;&lt;td&gt;constructor argument, &lt;code&gt;GridSearchCV&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Overfitting&lt;/td&gt;&lt;td&gt;Curve through every point&lt;/td&gt;&lt;td&gt;none, seen as a gap between training and test scores&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Baseline&lt;/td&gt;&lt;td&gt;Mean of past runs&lt;/td&gt;&lt;td&gt;&lt;code&gt;DummyRegressor&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Scaling&lt;/td&gt;&lt;td&gt;Comparable ranges for all columns&lt;/td&gt;&lt;td&gt;&lt;code&gt;StandardScaler&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Pipeline&lt;/td&gt;&lt;td&gt;Written procedure&lt;/td&gt;&lt;td&gt;&lt;code&gt;Pipeline&lt;/code&gt;, &lt;code&gt;make_pipeline&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Residual&lt;/td&gt;&lt;td&gt;Measured minus predicted&lt;/td&gt;&lt;td&gt;none, &lt;code&gt;y - model.predict(X)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;where-the-calibration-comparison-stops&quot;&gt;Where the calibration comparison stops&lt;/h2&gt;
&lt;p&gt;The calibration comparison works for what the words mean, and I think it stops working when you ask how much data you need. A calibration has one input and a known form, so a few standards are enough. A model with several features and no assumed form needs many more rows before it can say anything, and a run table from a pilot unit has few.&lt;/p&gt;
&lt;p&gt;One thing about the comparison above. I used the check standard for the test set and also for the fold held back inside cross-validation. The user guide keeps a separate evaluation set for the final score, one that was never seen while the settings were being searched,&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;scikit-learn user guide, section 3.2: “Hyper-parameters are parameters that are not directly learnt within estimators”, passed as arguments to the constructor; &lt;code&gt;GridSearchCV&lt;/code&gt; considers all combinations of given values and &lt;code&gt;RandomizedSearchCV&lt;/code&gt; samples a given number of candidates; the final evaluation is to be done on an evaluation set of samples not seen during the search.   &lt;/span&gt; so those two are different things and I have used one picture for both.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;scikit-learn glossary, entries “sample”, “feature” and “target”. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;scikit-learn user guide, section 3.1: the opening paragraph on testing a prediction function on its training data and on overfitting; the description of k-fold cross-validation; the Leave One Out section; and the passage stating that preprocessing such as standardization should be learnt from a training set and applied to held-out data. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;scikit-learn user guide, section 3.2: “Hyper-parameters are parameters that are not directly learnt within estimators”, passed as arguments to the constructor; &lt;code&gt;GridSearchCV&lt;/code&gt; considers all combinations of given values and &lt;code&gt;RandomizedSearchCV&lt;/code&gt; samples a given number of candidates; the final evaluation is to be done on an evaluation set of samples not seen during the search. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;scikit-learn API reference, &lt;code&gt;DummyRegressor&lt;/code&gt;: a regressor that makes predictions using simple rules, useful as a simple baseline; the default strategy “mean” always predicts the mean of the training set. &lt;a href=&quot;#user-content-fnref-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;scikit-learn, Common pitfalls and recommended practices, section on data leakage: the definition, the rule never to call &lt;code&gt;fit&lt;/code&gt; on the test data, and the recommendation of the pipeline for cross-validation and hyperparameter tuning. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Tree ensembles in plain terms</title>
    <link href="https://tishad.net/essays/tree-ensembles-in-plain-terms/" />
    <id>https://tishad.net/essays/tree-ensembles-in-plain-terms/</id>
    <published>2025-11-25T00:00:00.000Z</published>
    <updated>2025-11-25T00:00:00.000Z</updated>
    <summary>What a decision tree, a random forest, gradient boosting and XGBoost are, what their main settings mean, and why none of them can predict beyond the range of the runs they were trained on.</summary>
    <content type="html">&lt;p&gt;I met decision trees, random forests and XGBoost one evening in a course an assistant was giving me on the scikit-learn workflow. A few months later I ran a random forest, XGBoost and gradient boosting myself, on my own computer in VS Code. One install line brought all of them:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;pip install pandas numpy scikit-learn xgboost shap matplotlib seaborn&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that each model is a few lines of Python, so you can run all of them without knowing what they are. They are all built from the same piece, the decision tree.&lt;/p&gt;
&lt;h2 id=&quot;a-decision-tree&quot;&gt;A decision tree&lt;/h2&gt;
&lt;p&gt;A decision tree predicts a number by asking yes or no questions about the inputs, one after another. Say the first one is whether the temperature is above 800 C, and the next one is whether the feed is type A. Each answer sends the run down one branch, and at the end of the branches there is a leaf. The prediction for a run that lands in a leaf is the mean of the training runs that landed in the same leaf.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.10: a tree as a piecewise constant approximation; little data preparation; overfitting, instability and the statement that trees “are not good at extrapolation”; leaf values as the node mean under squared error; the tips on &lt;code&gt;max_depth&lt;/code&gt; and &lt;code&gt;min_samples_leaf&lt;/code&gt;.      &lt;/span&gt; It looks like a troubleshooting chart an operator would follow, only here the algorithm picks the questions and the thresholds, and it picks each one so the runs on either side are as alike in their result as possible.&lt;/p&gt;
&lt;p&gt;The scikit-learn guide calls the result a piecewise constant approximation.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.10: a tree as a piecewise constant approximation; little data preparation; overfitting, instability and the statement that trees “are not good at extrapolation”; leaf values as the node mean under squared error; the tips on &lt;code&gt;max_depth&lt;/code&gt; and &lt;code&gt;min_samples_leaf&lt;/code&gt;.      &lt;/span&gt; If you plot the prediction against one input you get a staircase and never a smooth curve.&lt;/p&gt;
&lt;p&gt;A tree assumes very little. It does not assume a straight line or any other form, and the guide says it needs little data preparation, where other methods often need the inputs normalized first.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.10: a tree as a piecewise constant approximation; little data preparation; overfitting, instability and the statement that trees “are not good at extrapolation”; leaf values as the node mean under squared error; the tips on &lt;code&gt;max_depth&lt;/code&gt; and &lt;code&gt;min_samples_leaf&lt;/code&gt;.      &lt;/span&gt; In scikit-learn it is &lt;code&gt;DecisionTreeRegressor&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The settings control how far the sorting goes. &lt;code&gt;max_depth&lt;/code&gt; is how many questions can come in a row, and &lt;code&gt;min_samples_leaf&lt;/code&gt; is the smallest number of training runs allowed in a leaf. If you put no limit, the tree keeps splitting until every training run sits in its own leaf, so it reproduces the training data and that is overfitting. The guide suggests starting with a depth of 3.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.10: a tree as a piecewise constant approximation; little data preparation; overfitting, instability and the statement that trees “are not good at extrapolation”; leaf values as the node mean under squared error; the tips on &lt;code&gt;max_depth&lt;/code&gt; and &lt;code&gt;min_samples_leaf&lt;/code&gt;.      &lt;/span&gt; It also says a tree is unstable, meaning a small change in the data can give a completely different tree, and that the way around it is to use trees inside an ensemble.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.10: a tree as a piecewise constant approximation; little data preparation; overfitting, instability and the statement that trees “are not good at extrapolation”; leaf values as the node mean under squared error; the tips on &lt;code&gt;max_depth&lt;/code&gt; and &lt;code&gt;min_samples_leaf&lt;/code&gt;.      &lt;/span&gt; An ensemble is many models combined into one, and the next three are ensembles of trees.&lt;/p&gt;
&lt;h2 id=&quot;a-random-forest&quot;&gt;A random forest&lt;/h2&gt;
&lt;p&gt;A random forest grows many trees and averages their predictions. Two things make the trees different from each other. Each tree is built on a bootstrap sample, which is a random draw of the training runs with replacement, so some runs appear twice and some do not appear. And at each split the tree may be allowed to look at only a random subset of the features. The guide says the purpose of both is to decrease the variance of the forest.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;scikit-learn user guide, section 1.11: bootstrap samples and random feature subsets in random forests, to decrease variance; &lt;code&gt;n_estimators&lt;/code&gt; and &lt;code&gt;max_features&lt;/code&gt;; gradient boosting as an additive model fitted to negative gradients; &lt;code&gt;learning_rate&lt;/code&gt; as shrinkage and its interaction with &lt;code&gt;n_estimators&lt;/code&gt;; &lt;code&gt;subsample&lt;/code&gt;.      &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;I think of it like replicate analyses. One determination has scatter, and the mean of many has less, as long as they do not all carry the same error. Breiman’s paper says the error of a forest depends on how strong the individual trees are and on the correlation between them,&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;Breiman (2001), abstract: generalization error converges to a limit as the number of trees becomes large and depends on the strength of the individual trees and the correlation between them.  &lt;/span&gt; so the randomness is there to make the trees less correlated.&lt;/p&gt;
&lt;p&gt;The main settings are &lt;code&gt;n_estimators&lt;/code&gt;, the number of trees, and &lt;code&gt;max_features&lt;/code&gt;, the size of the random subset at each split.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;scikit-learn user guide, section 1.11: bootstrap samples and random feature subsets in random forests, to decrease variance; &lt;code&gt;n_estimators&lt;/code&gt; and &lt;code&gt;max_features&lt;/code&gt;; gradient boosting as an additive model fitted to negative gradients; &lt;code&gt;learning_rate&lt;/code&gt; as shrinkage and its interaction with &lt;code&gt;n_estimators&lt;/code&gt;; &lt;code&gt;subsample&lt;/code&gt;.      &lt;/span&gt; Breiman showed that the error converges to a limit as you add trees,&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;Breiman (2001), abstract: generalization error converges to a limit as the number of trees becomes large and depends on the strength of the individual trees and the correlation between them.  &lt;/span&gt; so more trees cost you computing time and do not overfit. In a tuning search I ran later, the random forest was one of the slow models to tune.&lt;/p&gt;
&lt;h2 id=&quot;gradient-boosting&quot;&gt;Gradient boosting&lt;/h2&gt;
&lt;p&gt;Boosting also adds trees together, but it builds them one after another. Each new tree is fitted to what the trees before it still get wrong. In Friedman’s method the new tree is fitted to the negative gradient of the loss, and for squared error that is the current residual, measured minus predicted.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;Friedman (2001), abstract: function estimation as numerical optimization in function space, a general gradient descent boosting method for any fitting criterion, with specific algorithms for regression trees. &lt;/span&gt;&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;scikit-learn user guide, section 1.11: bootstrap samples and random feature subsets in random forests, to decrease variance; &lt;code&gt;n_estimators&lt;/code&gt; and &lt;code&gt;max_features&lt;/code&gt;; gradient boosting as an additive model fitted to negative gradients; &lt;code&gt;learning_rate&lt;/code&gt; as shrinkage and its interaction with &lt;code&gt;n_estimators&lt;/code&gt;; &lt;code&gt;subsample&lt;/code&gt;.      &lt;/span&gt; To me it is like trimming out an offset in small moves until it is gone.&lt;/p&gt;
&lt;p&gt;So the settings mean different things here. &lt;code&gt;n_estimators&lt;/code&gt; is the number of correction steps. &lt;code&gt;learning_rate&lt;/code&gt; is the fraction of each correction that is kept, and it is also called shrinkage. The guide says the two interact strongly, a smaller learning rate needs more trees, and small values favor better test error.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;scikit-learn user guide, section 1.11: bootstrap samples and random feature subsets in random forests, to decrease variance; &lt;code&gt;n_estimators&lt;/code&gt; and &lt;code&gt;max_features&lt;/code&gt;; gradient boosting as an additive model fitted to negative gradients; &lt;code&gt;learning_rate&lt;/code&gt; as shrinkage and its interaction with &lt;code&gt;n_estimators&lt;/code&gt;; &lt;code&gt;subsample&lt;/code&gt;.      &lt;/span&gt; &lt;code&gt;max_depth&lt;/code&gt; is the size of each small tree, and &lt;code&gt;subsample&lt;/code&gt; fits each tree on a random fraction of the runs.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;scikit-learn user guide, section 1.11: bootstrap samples and random feature subsets in random forests, to decrease variance; &lt;code&gt;n_estimators&lt;/code&gt; and &lt;code&gt;max_features&lt;/code&gt;; gradient boosting as an additive model fitted to negative gradients; &lt;code&gt;learning_rate&lt;/code&gt; as shrinkage and its interaction with &lt;code&gt;n_estimators&lt;/code&gt;; &lt;code&gt;subsample&lt;/code&gt;.      &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Boosting cuts the error down step by step, and if you give it enough steps it goes on to fit the noise in the training runs. So in boosting the number of trees is a setting you have to tune, and the guide treats shrinkage as a way to control overfitting.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;scikit-learn user guide, section 1.11: bootstrap samples and random feature subsets in random forests, to decrease variance; &lt;code&gt;n_estimators&lt;/code&gt; and &lt;code&gt;max_features&lt;/code&gt;; gradient boosting as an additive model fitted to negative gradients; &lt;code&gt;learning_rate&lt;/code&gt; as shrinkage and its interaction with &lt;code&gt;n_estimators&lt;/code&gt;; &lt;code&gt;subsample&lt;/code&gt;.      &lt;/span&gt; In scikit-learn it is &lt;code&gt;GradientBoostingRegressor&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;xgboost&quot;&gt;XGBoost&lt;/h2&gt;
&lt;p&gt;XGBoost is a separate library, which is why it has its own name in the install line, and the method inside it is gradient boosting of trees. Chen and Guestrin describe it as a scalable tree boosting system, and most of their paper is about speed on very large data, like handling sparse inputs and using memory and disk well.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-5&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;5&lt;/span&gt;Chen and Guestrin (2016), abstract and sections 2.1 and 2.3: the regularized objective penalizing the number of leaves and the leaf weights; shrinkage after Friedman and column subsampling as in random forests.   &lt;/span&gt; I do not think any of that matters for a table of a few tens of rows.&lt;/p&gt;
&lt;p&gt;What matters at any size is that XGBoost adds a penalty to the loss for how complex each tree is, counted by the number of leaves and the size of the leaf values. The authors say that with this term set to zero you are back to traditional gradient tree boosting.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-5&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;5&lt;/span&gt;Chen and Guestrin (2016), abstract and sections 2.1 and 2.3: the regularized objective penalizing the number of leaves and the leaf weights; shrinkage after Friedman and column subsampling as in random forests.   &lt;/span&gt; It also uses shrinkage from Friedman and it subsamples the feature columns like a random forest does.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-5&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;5&lt;/span&gt;Chen and Guestrin (2016), abstract and sections 2.1 and 2.3: the regularized objective penalizing the number of leaves and the leaf weights; shrinkage after Friedman and column subsampling as in random forests.   &lt;/span&gt; In Python the penalty is &lt;code&gt;reg_lambda&lt;/code&gt; and &lt;code&gt;gamma&lt;/code&gt;, the shrinkage is &lt;code&gt;learning_rate&lt;/code&gt; and the column subsampling is &lt;code&gt;colsample_bytree&lt;/code&gt;, next to the same &lt;code&gt;n_estimators&lt;/code&gt; and &lt;code&gt;max_depth&lt;/code&gt; as before.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-6&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;6&lt;/span&gt;XGBoost documentation, XGBoost parameters: &lt;code&gt;eta&lt;/code&gt; (alias &lt;code&gt;learning_rate&lt;/code&gt;), &lt;code&gt;max_depth&lt;/code&gt;, &lt;code&gt;colsample_bytree&lt;/code&gt;, &lt;code&gt;lambda&lt;/code&gt; (alias &lt;code&gt;reg_lambda&lt;/code&gt;) and &lt;code&gt;gamma&lt;/code&gt;. &lt;/span&gt; The model is &lt;code&gt;XGBRegressor&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;outside-the-training-range&quot;&gt;Outside the training range&lt;/h2&gt;
&lt;p&gt;Every one of these models ends in leaves, and a leaf holds a constant that came from training runs. Say a tree was trained on runs between 600 and 900 C and you ask it about 1000 C. The last question about temperature that the tree learned has its threshold somewhere below 900 C. The 1000 C point answers that question the same way a 900 C run does, so it lands in the same leaf and gets the same prediction. The staircase is flat after its last step. The scikit-learn guide says it directly, tree predictions are piecewise constant and so they are not good at extrapolation.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.10: a tree as a piecewise constant approximation; little data preparation; overfitting, instability and the statement that trees “are not good at extrapolation”; leaf values as the node mean under squared error; the tips on &lt;code&gt;max_depth&lt;/code&gt; and &lt;code&gt;min_samples_leaf&lt;/code&gt;.      &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Averaging does not change this, because an average of flat lines is flat, and a sum of small flat corrections is flat too. So a forest or a boosted model follows the trend up to the edge of the data and then holds the last value, and no setting fixes that. A straight line fit would at least keep its slope, right or wrong. A tree ensemble gives you the answer for the nearest condition it has seen, and it gives no sign that you asked outside its range. So before I believe a prediction from one of these, I compare the new condition with the minimum and maximum of each feature in the training table.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;scikit-learn user guide, section 1.10: a tree as a piecewise constant approximation; little data preparation; overfitting, instability and the statement that trees “are not good at extrapolation”; leaf values as the node mean under squared error; the tips on &lt;code&gt;max_depth&lt;/code&gt; and &lt;code&gt;min_samples_leaf&lt;/code&gt;. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-6&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;scikit-learn user guide, section 1.11: bootstrap samples and random feature subsets in random forests, to decrease variance; &lt;code&gt;n_estimators&lt;/code&gt; and &lt;code&gt;max_features&lt;/code&gt;; gradient boosting as an additive model fitted to negative gradients; &lt;code&gt;learning_rate&lt;/code&gt; as shrinkage and its interaction with &lt;code&gt;n_estimators&lt;/code&gt;; &lt;code&gt;subsample&lt;/code&gt;. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-6&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Breiman (2001), abstract: generalization error converges to a limit as the number of trees becomes large and depends on the strength of the individual trees and the correlation between them. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Friedman (2001), abstract: function estimation as numerical optimization in function space, a general gradient descent boosting method for any fitting criterion, with specific algorithms for regression trees. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;Chen and Guestrin (2016), abstract and sections 2.1 and 2.3: the regularized objective penalizing the number of leaves and the leaf weights; shrinkage after Friedman and column subsampling as in random forests. &lt;a href=&quot;#user-content-fnref-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-5-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 5-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-5-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 5-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-6&quot;&gt;
&lt;p&gt;XGBoost documentation, XGBoost parameters: &lt;code&gt;eta&lt;/code&gt; (alias &lt;code&gt;learning_rate&lt;/code&gt;), &lt;code&gt;max_depth&lt;/code&gt;, &lt;code&gt;colsample_bytree&lt;/code&gt;, &lt;code&gt;lambda&lt;/code&gt; (alias &lt;code&gt;reg_lambda&lt;/code&gt;) and &lt;code&gt;gamma&lt;/code&gt;. &lt;a href=&quot;#user-content-fnref-6&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>A small neural network in plain terms</title>
    <link href="https://tishad.net/essays/a-small-neural-network-in-plain-terms/" />
    <id>https://tishad.net/essays/a-small-neural-network-in-plain-terms/</id>
    <published>2025-11-19T00:00:00.000Z</published>
    <updated>2025-11-19T00:00:00.000Z</updated>
    <summary>What the units, layers, activation and epochs of a multilayer perceptron are, why its inputs are scaled first, and why the fit on a small table changes from one start to the next.</summary>
    <content type="html">&lt;p&gt;When I started on machine learning I had no understanding of neural networks, and I told the assistant so. About three months later a small neural network was one of the models I was fitting on my own computer.&lt;/p&gt;
&lt;p&gt;The network I mean is a multilayer perceptron, MLP for short. It is the plainest kind of neural network, and it is the one scikit-learn gives you for a table of numbers, under the name &lt;code&gt;MLPRegressor&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;units-and-layers&quot;&gt;Units and layers&lt;/h2&gt;
&lt;p&gt;Start with one unit. A unit takes the numbers handed to it, multiplies each one by a weight, adds them up, and then passes the sum through a fixed curve called the activation function.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.17: the weighted sum followed by an activation, the layers, the identity output and squared error for regression, training by backpropagation, the listed disadvantages, and the tips on scaling, pipelines and &lt;code&gt;alpha&lt;/code&gt;.        &lt;/span&gt; A weight is like a fitted coefficient in a regression. It is a number the fitting adjusts.&lt;/p&gt;
&lt;p&gt;Then the units are arranged in layers. The input layer has one unit for each input column, so if your table has a temperature and a reactor type, that is two units. The output layer gives the prediction, a yield for example. In between are the hidden layers, and each unit in a hidden layer takes its inputs from every unit in the layer before it.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.17: the weighted sum followed by an activation, the layers, the identity output and squared error for regression, training by backpropagation, the listed disadvantages, and the tips on scaling, pipelines and &lt;code&gt;alpha&lt;/code&gt;.        &lt;/span&gt; For regression the output unit is a plain weighted sum with no curve, and the fit minimizes the squared error, which is the same quantity a least squares line minimizes.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.17: the weighted sum followed by an activation, the layers, the identity output and squared error for regression, training by backpropagation, the listed disadvantages, and the tips on scaling, pipelines and &lt;code&gt;alpha&lt;/code&gt;.        &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The setting that fixes the shape is &lt;code&gt;hidden_layer_sizes&lt;/code&gt;. The value &lt;code&gt;(10, 10)&lt;/code&gt; means two hidden layers of ten units each. If you set nothing you get one layer of 100.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;MLPRegressor reference: &lt;code&gt;hidden_layer_sizes&lt;/code&gt; default (100,), &lt;code&gt;activation&lt;/code&gt; options and default relu, the &lt;code&gt;solver&lt;/code&gt; note on small datasets and lbfgs, &lt;code&gt;alpha&lt;/code&gt; default 0.0001, &lt;code&gt;max_iter&lt;/code&gt; default 200, &lt;code&gt;tol&lt;/code&gt;, &lt;code&gt;random_state&lt;/code&gt;.      &lt;/span&gt; The count of adjustable numbers grows fast. Say there are three inputs, two hidden layers of ten units and one output. Then I count 40 weights and offsets for the first hidden layer, 110 for the second and 11 for the output, so 161 in all. On a table of a few dozen runs, even a small network like this one has several times more adjustable numbers than there are rows.&lt;/p&gt;
&lt;p&gt;This setting also gave me a bug. I had saved the layer sizes of tuned networks in a CSV as text like “10_10”. When the file was loaded again, the loader read that column as integers, so “10_10” became 1010 and “20_10” became 2010, and no error was thrown. I found it later, fixed the values and ran the predictions again.&lt;/p&gt;
&lt;h2 id=&quot;the-activation&quot;&gt;The activation&lt;/h2&gt;
&lt;p&gt;Without the curve the layers would do nothing, because a weighted sum of weighted sums is still a weighted sum, so the whole network would be a linear model however many layers it had. The activation bends the output of each unit, and many bent pieces stacked together can follow a curved response, like a yield that rises with temperature and then levels off. In scikit-learn the choices are identity, logistic, tanh and relu, and relu is the default.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;MLPRegressor reference: &lt;code&gt;hidden_layer_sizes&lt;/code&gt; default (100,), &lt;code&gt;activation&lt;/code&gt; options and default relu, the &lt;code&gt;solver&lt;/code&gt; note on small datasets and lbfgs, &lt;code&gt;alpha&lt;/code&gt; default 0.0001, &lt;code&gt;max_iter&lt;/code&gt; default 200, &lt;code&gt;tol&lt;/code&gt;, &lt;code&gt;random_state&lt;/code&gt;.      &lt;/span&gt; Relu passes a positive sum through unchanged and sets a negative one to zero.&lt;/p&gt;
&lt;h2 id=&quot;training-and-epochs&quot;&gt;Training and epochs&lt;/h2&gt;
&lt;p&gt;The weights start as random numbers. Training shows the network the rows, measures the error, and works out for every weight which direction would reduce the error. Then the weights move a small step and the cycle repeats. The procedure that passes the error back through the layers is called backpropagation.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.17: the weighted sum followed by an activation, the layers, the identity output and squared error for regression, training by backpropagation, the listed disadvantages, and the tips on scaling, pipelines and &lt;code&gt;alpha&lt;/code&gt;.        &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;An epoch is one pass through all the training rows. In scikit-learn the setting &lt;code&gt;max_iter&lt;/code&gt; caps the number of passes at 200 by default.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;MLPRegressor reference: &lt;code&gt;hidden_layer_sizes&lt;/code&gt; default (100,), &lt;code&gt;activation&lt;/code&gt; options and default relu, the &lt;code&gt;solver&lt;/code&gt; note on small datasets and lbfgs, &lt;code&gt;alpha&lt;/code&gt; default 0.0001, &lt;code&gt;max_iter&lt;/code&gt; default 200, &lt;code&gt;tol&lt;/code&gt;, &lt;code&gt;random_state&lt;/code&gt;.      &lt;/span&gt; Training stops earlier if the error stops improving by more than a tolerance. If it reaches the cap first, the library prints a convergence warning. On one long job on my PC the terminal printed those warnings the whole time. As I understand it, the warning means the fit was stopped before it settled, and the usual answer is more iterations or a different solver. The documentation says the default solver, adam, works well on datasets with thousands of rows, and that for small datasets lbfgs can converge faster and perform better.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;MLPRegressor reference: &lt;code&gt;hidden_layer_sizes&lt;/code&gt; default (100,), &lt;code&gt;activation&lt;/code&gt; options and default relu, the &lt;code&gt;solver&lt;/code&gt; note on small datasets and lbfgs, &lt;code&gt;alpha&lt;/code&gt; default 0.0001, &lt;code&gt;max_iter&lt;/code&gt; default 200, &lt;code&gt;tol&lt;/code&gt;, &lt;code&gt;random_state&lt;/code&gt;.      &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Tuning a network is also slow, and the size of the table does not tell you that in advance. In my tuning search the MLP was one of the slow models.&lt;/p&gt;
&lt;h2 id=&quot;scaling-the-inputs&quot;&gt;Scaling the inputs&lt;/h2&gt;
&lt;p&gt;A furnace temperature runs to several hundred degrees. A reactor type coded as 0 or 1 never goes above one. If both go raw into a weighted sum, the temperature swamps the code. The user guide says the model is sensitive to feature scaling, so each input should be brought to a common range or standardized to mean 0 and variance 1, and the same scaling has to be applied to the rows held out for testing.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.17: the weighted sum followed by an activation, the layers, the identity output and squared error for regression, training by backpropagation, the listed disadvantages, and the tips on scaling, pipelines and &lt;code&gt;alpha&lt;/code&gt;.        &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The part that is easy to get wrong is where the scaling is learned. If the mean and the spread are computed on all the rows before validation, then the held-out row has already had a say in the model. So the guide recommends putting the scaler and the network in one pipeline,&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.17: the weighted sum followed by an activation, the layers, the identity output and squared error for regression, training by backpropagation, the listed disadvantages, and the tips on scaling, pipelines and &lt;code&gt;alpha&lt;/code&gt;.        &lt;/span&gt; and then each validation split learns its scaling from its own training rows.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sklearn.pipeline &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; make_pipeline&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sklearn.preprocessing &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; StandardScaler&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sklearn.neural_network &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; MLPRegressor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;model &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; make_pipeline(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    StandardScaler(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    MLPRegressor(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;hidden_layer_sizes&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;max_iter&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;random_state&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;why-the-fit-changes-from-one-start-to-the-next&quot;&gt;Why the fit changes from one start to the next&lt;/h2&gt;
&lt;p&gt;The user guide lists this as a disadvantage. A network with hidden layers has an error surface with more than one minimum, so different random starting weights can end in different fits with different validation scores.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.17: the weighted sum followed by an activation, the layers, the identity output and squared error for regression, training by backpropagation, the listed disadvantages, and the tips on scaling, pipelines and &lt;code&gt;alpha&lt;/code&gt;.        &lt;/span&gt; On a small table I think it is easy to see why. There are more adjustable numbers than rows, so many different sets of weights reproduce the training rows about equally well, and they do not agree with each other in between those rows. Which set you get depends on where the training started.&lt;/p&gt;
&lt;p&gt;Setting &lt;code&gt;random_state&lt;/code&gt; to a fixed integer makes one fit repeatable.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;MLPRegressor reference: &lt;code&gt;hidden_layer_sizes&lt;/code&gt; default (100,), &lt;code&gt;activation&lt;/code&gt; options and default relu, the &lt;code&gt;solver&lt;/code&gt; note on small datasets and lbfgs, &lt;code&gt;alpha&lt;/code&gt; default 0.0001, &lt;code&gt;max_iter&lt;/code&gt; default 200, &lt;code&gt;tol&lt;/code&gt;, &lt;code&gt;random_state&lt;/code&gt;.      &lt;/span&gt; That gives you the same start every time, and it says nothing about whether another start would have done better. The other handle is &lt;code&gt;alpha&lt;/code&gt;, a penalty on large weights that defaults to 0.0001, and the guide suggests searching it over several orders of magnitude.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;scikit-learn user guide, section 1.17: the weighted sum followed by an activation, the layers, the identity output and squared error for regression, training by backpropagation, the listed disadvantages, and the tips on scaling, pipelines and &lt;code&gt;alpha&lt;/code&gt;.        &lt;/span&gt;&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;MLPRegressor reference: &lt;code&gt;hidden_layer_sizes&lt;/code&gt; default (100,), &lt;code&gt;activation&lt;/code&gt; options and default relu, the &lt;code&gt;solver&lt;/code&gt; note on small datasets and lbfgs, &lt;code&gt;alpha&lt;/code&gt; default 0.0001, &lt;code&gt;max_iter&lt;/code&gt; default 200, &lt;code&gt;tol&lt;/code&gt;, &lt;code&gt;random_state&lt;/code&gt;.      &lt;/span&gt;&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;scikit-learn user guide, section 1.17: the weighted sum followed by an activation, the layers, the identity output and squared error for regression, training by backpropagation, the listed disadvantages, and the tips on scaling, pipelines and &lt;code&gt;alpha&lt;/code&gt;. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-6&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-7&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-7&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-8&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-8&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;MLPRegressor reference: &lt;code&gt;hidden_layer_sizes&lt;/code&gt; default (100,), &lt;code&gt;activation&lt;/code&gt; options and default relu, the &lt;code&gt;solver&lt;/code&gt; note on small datasets and lbfgs, &lt;code&gt;alpha&lt;/code&gt; default 0.0001, &lt;code&gt;max_iter&lt;/code&gt; default 200, &lt;code&gt;tol&lt;/code&gt;, &lt;code&gt;random_state&lt;/code&gt;. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2-6&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Starting Python and machine learning from zero with an assistant as the teacher</title>
    <link href="https://tishad.net/essays/starting-python-and-machine-learning-from-zero/" />
    <id>https://tishad.net/essays/starting-python-and-machine-learning-from-zero/</id>
    <published>2025-11-19T00:00:00.000Z</published>
    <updated>2025-11-19T00:00:00.000Z</updated>
    <summary>The first days of learning Python and machine learning as a chemical engineer with no coding, with the questions the assistant asked, the plan it gave and what two evenings covered.</summary>
    <content type="html">&lt;p&gt;I am a chemical engineer with a PhD, and when this started I had no coding. I had never written Python and I did not understand what a neural network was. I wanted to do research with machine learning, and with PINNs in particular. PINN stands for physics-informed neural network. It is a neural network where the governing equation of the problem, for example the rate equation of a reactor, goes into the loss, which is the number the training tries to make small.&lt;/p&gt;
&lt;p&gt;So at 4 AM one night I asked Claude to teach me machine learning, AI and PINNs from the ground up, as a PhD chemical engineer new to all of it, with examples I can relate to.&lt;/p&gt;
&lt;p&gt;It asked me three questions before it started. My answers were: no experience with Python, no understanding of neural networks, and I want both fundamentals and applications. Then it gave me three paths to choose from. Option A was foundation first for 2 to 3 weeks, option B was learn by doing from week 1, and option C was a parallel track. I chose option C, and the first install was going to be Anaconda.&lt;/p&gt;
&lt;p&gt;The next day I asked for an online platform instead of a local install and went with &lt;a href=&quot;https://colab.research.google.com/&quot;&gt;Google Colab&lt;/a&gt;, which is a Python notebook that runs in the browser. I ran the first cell and saw output. Then I asked for a simple ODE problem, so my first small model was a PINN for a first-order reactor, built in parts: the problem, the network, the physics-informed loss, the training. Training completed and I plotted the results. Colab removes the install step entirely, and I think that is why the first working model took one evening.&lt;/p&gt;
&lt;p&gt;A few days later, close to midnight, I started a proper course in a new chat. I asked again, a little differently: teach me the fundamentals and applications of machine learning and deep learning, as a PhD who did not study computer engineering and has done little or no coding, enough to do ML and PINN research. The plan it gave had five phases.&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Phase&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;Time&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Python&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;2 to 3 weeks&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mathematics&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;1 to 2 weeks&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Classical ML&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;3 to 4 weeks&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Deep learning&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;4 to 6 weeks&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PINNs&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;3 to 4 weeks&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;We started with Python basics on Colab: variables, operations, lists, dictionaries, control flow and functions. Then I went straight on to NumPy the same night, which is the library for arrays, so indexing, operations on arrays, statistics, linear algebra, broadcasting and random numbers.&lt;/p&gt;
&lt;p&gt;The next evening I did pandas, from series and dataframes through pivot tables and applying functions. Then matplotlib and seaborn for plots, subplots, error bars, heatmaps and pair plots. Then the scikit-learn workflow, which started with exploring and preprocessing the data and went on to linear regression, polynomial features, ridge and lasso, decision trees, random forests, XGBoost and support vector regression, and after that model comparison, cross-validation and hyperparameter tuning.&lt;/p&gt;
&lt;p&gt;So in two evenings I had gone through the topics of the Python phase, which the plan gave 2 to 3 weeks, and I think a good part of what it called classical ML.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Starting Python twice as an engineer who had never programmed</title>
    <link href="https://tishad.net/essays/starting-python-twice-as-an-engineer/" />
    <id>https://tishad.net/essays/starting-python-twice-as-an-engineer/</id>
    <published>2025-11-19T00:00:00.000Z</published>
    <updated>2025-11-19T00:00:00.000Z</updated>
    <summary>How I started Python twice, first in a hosted notebook with nothing installed and then on my own computer where the setup took most of the effort, with the errors I hit and the order I would install things in now.</summary>
    <content type="html">&lt;p&gt;I have a PhD in chemical engineering and had done no programming when I asked Claude to teach me machine learning from the ground up, with examples I can relate to. It asked me a few things first, and my answers were that I had no experience with Python and no understanding of neural networks, and that I wanted both the fundamentals and the applications. The plan I picked had Anaconda as the first install. Anaconda is a large bundle of Python and scientific libraries. I did not install it then, so I cannot say anything for or against it.&lt;/p&gt;
&lt;h2 id=&quot;in-the-browser-first&quot;&gt;In the browser first&lt;/h2&gt;
&lt;p&gt;The next day I asked for an online platform instead of a local install, and went with Google Colab. Colab calls itself a hosted Jupyter Notebook service that requires no setup.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Google, Colaboratory FAQ: “Colab is a hosted Jupyter Notebook service that requires no setup to use”; “Colab resources are not guaranteed and not unlimited”.  &lt;/span&gt; A notebook is a page of boxes called cells. You type code in a cell, run it, and the result shows under it. I ran the first cell and saw output.&lt;/p&gt;
&lt;p&gt;A few days later I started a proper course with Claude, close to midnight, and all of it was in Colab. The order was the language itself first (variables, operations, lists, dictionaries, control flow, functions), then NumPy for arrays. The next evening it was pandas for tables, then matplotlib and seaborn for plots, then the scikit-learn workflow for fitting models. If you think in spreadsheets, a pandas dataframe is close to a worksheet with named columns.&lt;/p&gt;
&lt;p&gt;So in that first week nothing was installed on my computer, and I think that was right for me. The machine is Google’s, and they say the resources are not guaranteed and not unlimited,&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Google, Colaboratory FAQ: “Colab is a hosted Jupyter Notebook service that requires no setup to use”; “Colab resources are not guaranteed and not unlimited”.  &lt;/span&gt; but for learning the language that did not matter.&lt;/p&gt;
&lt;h2 id=&quot;on-my-own-computer&quot;&gt;On my own computer&lt;/h2&gt;
&lt;p&gt;About two and a half months later I wanted to do a small analysis myself, step by step, in VS Code. It was public employment data on chemical engineers. I already had VS Code from the day before. Claude had built a first version of the analysis, and then I insisted on doing all of it on my own computer.&lt;/p&gt;
&lt;p&gt;The setup was a fight. First I typed “matpilotlib” in the pip install line. Then pandas was not found inside the virtual environment I had made, and the message was:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Python was not found&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows has a command named &lt;code&gt;python&lt;/code&gt; that is only a shortcut to the Microsoft Store, and it was getting in front of the Python in my environment. The Python documentation for Windows now recommends the Python install manager, and it has a troubleshooting entry for this case that points to the “Manage app execution aliases” setting.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Python documentation, Using Python on Windows: the Python install manager, and the troubleshooting entry for &lt;code&gt;python&lt;/code&gt; giving “command not found” or opening the Store app. &lt;/span&gt; I installed Python 3.14.3 through the install manager and added its folder to PATH. Then I could not remove the old environment folder because it was still in use. Then pip wanted an upgrade, from 25.3 to 26.0.1. After that it said “All good”, and I had a Jupyter server running on localhost:8888.&lt;/p&gt;
&lt;p&gt;A virtual environment, or venv, is a folder that holds the libraries one project needs, kept apart from every other project.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;Python documentation, venv: what a virtual environment is, the creation and activation commands, and the note on &lt;code&gt;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser&lt;/code&gt;.  &lt;/span&gt; The reason is that two projects can need different versions of the same library, and one installation of Python holds only one of them.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;The Python tutorial, chapter 12: applications that need different versions of the same library, and &lt;code&gt;python -m pip install&lt;/code&gt;. &lt;/span&gt; It is like keeping a separate set of fittings for each skid, so work on one leaves the other alone. You make it with three lines in the terminal, typed in the project folder:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;python -m venv .venv&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;.venv\Scripts\Activate.ps1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;python -m pip install pandas numpy scikit-learn matplotlib jupyter&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line makes the environment, the second switches the terminal to it, and the third installs into it. On Windows the second line can be refused because PowerShell does not allow scripts by default. I had met that the day before with npm, where the message was “running scripts is disabled”, and the fix is the same one the venv documentation gives:&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;Python documentation, venv: what a virtual environment is, the creation and activation commands, and the note on &lt;code&gt;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser&lt;/code&gt;.  &lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two days after that I moved another analysis from the browser to my own computer, because waiting for Claude to run it there was taking too long. It was the same steps again, a venv and a pip install, and then a script file. I switched to Jupyter when I saw it would be easier cell by cell.&lt;/p&gt;
&lt;p&gt;The day after, I started in a new folder and ran the first script from a plain PowerShell window. It failed on a unicode escape in the script’s own docstring. Then it failed because pandas was not there, and then pip was not on the path in that window at all. So I went back to Jupyter in the venv, and told Claude to stop making me repeat my setup every time.&lt;/p&gt;
&lt;p&gt;I think the pandas and pip failures were the same problem, that the plain PowerShell window was not using the Python my libraries were installed into. What I wrote down that night was to set up the venv in Jupyter from the start and skip the PowerShell detour.&lt;/p&gt;
&lt;h2 id=&quot;the-order-i-would-use-now&quot;&gt;The order I would use now&lt;/h2&gt;
&lt;p&gt;If I started again I think I would do it in the same order. First a hosted notebook, until the language, NumPy, pandas and plotting are familiar. Then on my own computer I would install Python itself from python.org, then VS Code, then make a project folder with a venv in it, then install the libraries with pip inside that venv, and Jupyter inside the same venv.&lt;/p&gt;
&lt;p&gt;Git I would leave for later. I met it in that same local week, because the analysis was going to a public repository, and it took a .gitignore file, setting my name and email, and one commit. It is useful, but I did not need it to get a first plot.&lt;/p&gt;
&lt;p&gt;I should also say that in both weeks Claude wrote most of the code. I ran it and asked what each part did. I am not sure how much of it I could have written from a blank page at that time.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Google, Colaboratory FAQ: “Colab is a hosted Jupyter Notebook service that requires no setup to use”; “Colab resources are not guaranteed and not unlimited”. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Python documentation, Using Python on Windows: the Python install manager, and the troubleshooting entry for &lt;code&gt;python&lt;/code&gt; giving “command not found” or opening the Store app. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Python documentation, venv: what a virtual environment is, the creation and activation commands, and the note on &lt;code&gt;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser&lt;/code&gt;. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;The Python tutorial, chapter 12: applications that need different versions of the same library, and &lt;code&gt;python -m pip install&lt;/code&gt;. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>My local model failed on its first long document because it was loaded with a 4096 token context</title>
    <link href="https://tishad.net/log/my-local-model-failed-on-its-first-long-document/" />
    <id>https://tishad.net/log/my-local-model-failed-on-its-first-long-document/</id>
    <published>2025-11-16T00:00:00.000Z</published>
    <updated>2025-11-16T00:00:00.000Z</updated>
    <summary>The first long paper I gave a local model through AnythingLLM failed with a context overflow error, and reloading the model in LM Studio with a larger context length fixed it.</summary>
    <content type="html">&lt;p&gt;I had a model running offline on my own computer in LM Studio, and I was wondering if a local model can learn from online resources. I asked an assistant, and the options were web search added on top of the local model, RAG over my own documents, or fine-tuning. RAG means a program searches your documents for the passages that match your question and hands those passages to the model along with the question, so the model itself is not changed. I said yes to that one.&lt;/p&gt;
&lt;p&gt;So I enabled the local server in LM Studio, installed AnythingLLM, connected it to the server, made a workspace and uploaded research papers. The first long document failed with this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Trying to keep the first 7280 tokens when context the overflows. However, the model is loaded with context length of only 4096 tokens.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model was loaded with a context length of 4096 tokens, and the document needed 7280. The fix was to eject the model in LM Studio, reload it with a larger context length, and then update the AnythingLLM connection. I think context length is the first setting to raise before feeding a local model documents.&lt;/p&gt;
&lt;p&gt;At that point I did not know what a token was. I asked two days later. A token is a piece of text the model counts in, often a whole short word or a part of a longer one, so a document has more tokens than words. Context length is how many tokens the model can hold at one time, and the question, the passages from the documents and the answer all have to fit in it.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Colab, Jupyter or VS Code, and which one for which job</title>
    <link href="https://tishad.net/essays/colab-jupyter-or-vs-code-and-which-one-for-which-job/" />
    <id>https://tishad.net/essays/colab-jupyter-or-vs-code-and-which-one-for-which-job/</id>
    <published>2025-11-16T00:00:00.000Z</published>
    <updated>2025-11-16T00:00:00.000Z</updated>
    <summary>What a notebook, a cell, a kernel and a virtual environment are, for an engineer who has never programmed, and what I ended up using Colab, Jupyter and VS Code for.</summary>
    <content type="html">&lt;p&gt;When I started Python I had never programmed, and the names Colab, Jupyter and VS Code kept coming up. I ended up using all of them, each for a different job. Four words come with them: notebook, cell, kernel and virtual environment. I understand them by things I already know as a chemical engineer.&lt;/p&gt;
&lt;h2 id=&quot;notebook-cell-kernel-virtual-environment&quot;&gt;Notebook, cell, kernel, virtual environment&lt;/h2&gt;
&lt;p&gt;A notebook is a document that keeps the code, the output of the code and your own notes on one page. On disk it is one file ending in &lt;code&gt;.ipynb&lt;/code&gt;.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Jupyter Notebook documentation, “The Jupyter Notebook”. &lt;a href=&quot;https://jupyter-notebook.readthedocs.io/en/stable/notebook.html&quot;&gt;https://jupyter-notebook.readthedocs.io/en/stable/notebook.html&lt;/a&gt;    &lt;/span&gt; The closest thing I know is a run sheet, where the instruction, the reading and the remark sit next to each other.&lt;/p&gt;
&lt;p&gt;A cell is one box in that document. You write a few lines of code in it, you run it, and the result shows right under it.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Jupyter Notebook documentation, “The Jupyter Notebook”. &lt;a href=&quot;https://jupyter-notebook.readthedocs.io/en/stable/notebook.html&quot;&gt;https://jupyter-notebook.readthedocs.io/en/stable/notebook.html&lt;/a&gt;    &lt;/span&gt; So a cell is like one step in a procedure. You do the step, you look at the reading, then you go to the next step.&lt;/p&gt;
&lt;p&gt;The kernel is the thing that actually does the calculation. The page you see only sends the code of a cell to the kernel and shows what comes back.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Jupyter Notebook documentation, “The Jupyter Notebook”. &lt;a href=&quot;https://jupyter-notebook.readthedocs.io/en/stable/notebook.html&quot;&gt;https://jupyter-notebook.readthedocs.io/en/stable/notebook.html&lt;/a&gt;    &lt;/span&gt; The Jupyter documentation describes kernels as separate processes that run on their own and talk to the notebook.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Jupyter documentation, “Kernels (programming languages)”. &lt;a href=&quot;https://docs.jupyter.org/en/latest/projects/kernels.html&quot;&gt;https://docs.jupyter.org/en/latest/projects/kernels.html&lt;/a&gt; &lt;/span&gt; I think of the page as the display and the kernel as the instrument behind it. The instrument keeps its state, so a data table you loaded in cell 3 is still in memory when you run cell 10. If you restart the kernel it is like switching the instrument off and on. Everything in memory is gone and you run the cells again, although the old outputs are still printed on the page.&lt;/p&gt;
&lt;p&gt;A virtual environment, or venv, is a folder that holds one copy of Python and the libraries one project needs, kept apart from the other projects and from the Python of the computer itself.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;Python 3 documentation, “venv, creation of virtual environments”. &lt;a href=&quot;https://docs.python.org/3/library/venv.html&quot;&gt;https://docs.python.org/3/library/venv.html&lt;/a&gt; &lt;/span&gt; It is like the toolbox you keep with one rig, so nothing gets borrowed for another job. The kernel of a notebook on your own computer runs from one of these folders, and that decides which libraries the notebook can see.&lt;/p&gt;
&lt;h2 id=&quot;colab-for-the-first-evening&quot;&gt;Colab for the first evening&lt;/h2&gt;
&lt;p&gt;For my first Python I asked for an online platform because I did not want to install anything, and I went with Google Colab. Colab is a notebook service hosted by Google that needs no setup, and the notebooks are stored in Google Drive.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;Google, “Colaboratory frequently asked questions”. &lt;a href=&quot;https://research.google.com/colaboratory/faq.html&quot;&gt;https://research.google.com/colaboratory/faq.html&lt;/a&gt;  &lt;/span&gt; So the kernel runs on a machine at Google and you only need a browser. I ran the first cell and saw output, and the same evening I did a small tutorial problem on a first-order reactor in it. Later I did Python basics and NumPy in Colab too. Colab removes the install step entirely. The machine at Google is deleted after it sits idle for a while,&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-4&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;4&lt;/span&gt;Google, “Colaboratory frequently asked questions”. &lt;a href=&quot;https://research.google.com/colaboratory/faq.html&quot;&gt;https://research.google.com/colaboratory/faq.html&lt;/a&gt;  &lt;/span&gt; so I would not leave a long run on it.&lt;/p&gt;
&lt;h2 id=&quot;vs-code-for-the-folder-and-the-terminal&quot;&gt;VS Code for the folder and the terminal&lt;/h2&gt;
&lt;p&gt;VS Code is a general editor. It shows a project folder and its files, and it has a terminal built in. I already had it for LaTeX. So when I wanted to do an analysis of public employment data on my own computer, step by step and by myself, VS Code was the place I already knew.&lt;/p&gt;
&lt;p&gt;That evening was mostly a fight with the environment, a typo in a pip install and a Python that was not the one I meant, and when it was over the Jupyter server came up on localhost:8888, which is an address on your own computer and nothing on the internet.&lt;/p&gt;
&lt;p&gt;VS Code can also open and run notebooks itself through its Jupyter extension.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-5&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;5&lt;/span&gt;Visual Studio Code documentation, “Jupyter Notebooks in VS Code”. &lt;a href=&quot;https://code.visualstudio.com/docs/datascience/jupyter-notebooks&quot;&gt;https://code.visualstudio.com/docs/datascience/jupyter-notebooks&lt;/a&gt; &lt;/span&gt; I ran mine in a browser tab with VS Code beside it for the folder and the terminal.&lt;/p&gt;
&lt;h2 id=&quot;jupyter-when-i-had-to-rerun-the-work-myself&quot;&gt;Jupyter when I had to rerun the work myself&lt;/h2&gt;
&lt;p&gt;The next job was a modeling exercise. Claude was running it in the browser and it was taking too long, so I stopped it and moved the work to my own computer, one step at a time. I made a venv, installed the libraries, and started with a script, which is a single file that runs from top to bottom.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;pip install pandas numpy scikit-learn xgboost shap matplotlib seaborn&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I switched to Jupyter when I saw it would be easier cell by cell. With a script, if something fails near the end you run the whole file again. With a notebook the loaded data stays in the kernel, so you fix the one cell that failed and run only that. The next day I tried a script again in plain PowerShell and it failed because pandas and pip were not there outside the venv, so I went back to Jupyter in the venv. More than once at night I asked how to stop the notebook server and the venv.&lt;/p&gt;
&lt;h2 id=&quot;what-went-wrong-with-long-runs&quot;&gt;What went wrong with long runs&lt;/h2&gt;
&lt;p&gt;One cell printed nothing for 25 minutes. I did not know if it was working, so I left it overnight, and I asked whether the PC going to sleep would stop it. When I checked the next evening it had finished, and no files had been written. All the results were only in the memory of the kernel, so I had them saved to a CSV right then.&lt;/p&gt;
&lt;p&gt;Another long run died when I closed the browser by accident. The Jupyter documentation says the kernel stays active when the browser window is closed, and you can open the notebook again and reconnect.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Jupyter Notebook documentation, “The Jupyter Notebook”. &lt;a href=&quot;https://jupyter-notebook.readthedocs.io/en/stable/notebook.html&quot;&gt;https://jupyter-notebook.readthedocs.io/en/stable/notebook.html&lt;/a&gt;    &lt;/span&gt; I do not know if my kernel was still running and I just did not reconnect.&lt;/p&gt;
&lt;p&gt;Another time the PC went to sleep in the middle of a run. After the restart a cell gave a NameError on a variable that an earlier cell had defined, because the earlier cells had not been run again.&lt;/p&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Jupyter Notebook documentation, “The Jupyter Notebook”. &lt;a href=&quot;https://jupyter-notebook.readthedocs.io/en/stable/notebook.html&quot;&gt;https://jupyter-notebook.readthedocs.io/en/stable/notebook.html&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1-4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Jupyter documentation, “Kernels (programming languages)”. &lt;a href=&quot;https://docs.jupyter.org/en/latest/projects/kernels.html&quot;&gt;https://docs.jupyter.org/en/latest/projects/kernels.html&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Python 3 documentation, “venv, creation of virtual environments”. &lt;a href=&quot;https://docs.python.org/3/library/venv.html&quot;&gt;https://docs.python.org/3/library/venv.html&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Google, “Colaboratory frequently asked questions”. &lt;a href=&quot;https://research.google.com/colaboratory/faq.html&quot;&gt;https://research.google.com/colaboratory/faq.html&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-4-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;Visual Studio Code documentation, “Jupyter Notebooks in VS Code”. &lt;a href=&quot;https://code.visualstudio.com/docs/datascience/jupyter-notebooks&quot;&gt;https://code.visualstudio.com/docs/datascience/jupyter-notebooks&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>When a long chat hit its limit I had it write a handoff file for the next one</title>
    <link href="https://tishad.net/log/when-a-long-chat-hit-its-limit-i-had-it-write-a-handoff-file/" />
    <id>https://tishad.net/log/when-a-long-chat-hit-its-limit-i-had-it-write-a-handoff-file/</id>
    <published>2025-11-09T00:00:00.000Z</published>
    <updated>2025-11-09T00:00:00.000Z</updated>
    <summary>A long chat with Claude reached its length limit, so I had it write a markdown handoff file and started a fresh chat from that.</summary>
    <content type="html">&lt;p&gt;I was putting together a package of documents with Claude, over more than one chat. One afternoon I picked it up again, so I asked Claude to retrieve where the last chat ended and deliver the complete set of documents for Word. It did that, with a summary.&lt;/p&gt;
&lt;p&gt;Then the chat hit its length limit. A long chat fills up, and a new chat starts empty and cannot see the old one, so I was about to lose the context.&lt;/p&gt;
&lt;p&gt;So before leaving it, I had Claude write a handoff file in markdown, meant for a fresh tab, with the instruction to look at the project files. A handoff note written at the end carries the work into the next chat.&lt;/p&gt;
&lt;p&gt;Nine days later I did the same thing in a different chat, on a research idea. I had it write a summary of the conversation as a markdown file to continue elsewhere, then in another chat I asked for step by step instructions and started on step 1. That same day I had also asked Claude what tokens are, and got the explanation of tokens against words and what context length means.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Checking the numbers an assistant reads out of a long PDF</title>
    <link href="https://tishad.net/essays/checking-the-numbers-an-assistant-reads-out-of-a-long-pdf/" />
    <id>https://tishad.net/essays/checking-the-numbers-an-assistant-reads-out-of-a-long-pdf/</id>
    <published>2025-11-06T00:00:00.000Z</published>
    <updated>2025-11-06T00:00:00.000Z</updated>
    <summary>I gave Claude a long technical document of my own and it reported a number I still have to check, and later I caught things missing from what it put together from the same material.</summary>
    <content type="html">&lt;p&gt;The first evening I took Claude seriously for my profession, one of the things I gave it was a long technical document of my own. I uploaded the workbook of results that goes with it first, the spreadsheet where the runs are reduced to conditions and results, and then the document itself as a PDF. It saw more in the document than in the workbook. I think that is expected, because the workbook only holds the results, and the methods and the characterization are written in the document.&lt;/p&gt;
&lt;p&gt;Then it gave me a package of documents built on it, and the figure it made much of was one measured value. I wrote that document, and I still have not opened it and checked that number against the page.&lt;/p&gt;
&lt;p&gt;In the same month I asked what tokens are, because I did not know, and got the explanation of tokens against words and what context length means. The way I keep it in my head as a chemical engineer: tokens are the pieces of words the model works in, and the context window is a vessel of fixed volume. The whole PDF goes into that vessel together with everything else in the conversation. Anthropic’s documentation says each page of a PDF is processed as text and as an image, that a dense PDF with small fonts, complex tables or heavy graphics can fill the context window before the page limit is reached, and it suggests splitting a large document into sections.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;1&lt;/span&gt;Anthropic, “PDF support”. &lt;/span&gt; A long report full of tables is that kind of document. There is also a study by Liu and others where models had to find one relevant item in a long input, and they did best when the item was at the beginning or the end and worse when it was in the middle.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-2&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;2&lt;/span&gt;Liu et al. (2023), abstract. &lt;/span&gt; The results chapters of a long report are in the middle. So I cannot tell from the reply whether a number was read from the right row of the right table.&lt;/p&gt;
&lt;p&gt;About three months later I went back to the same material with Claude, and this time I caught a few things. A block of the data was missing from the file it had consolidated, so I uploaded the source file for that block again. Then one component that matters in that work was missing from the charts it drew. For one table I gave it screenshots of the pages instead of the file.&lt;/p&gt;
&lt;p&gt;A few days before that, in another project, I had asked for a literature review and for one paper to reproduce. The link in its citation pointed to the wrong paper, and I had already downloaded it on its word. I told it that it needs to learn to cite its claims correctly.&lt;/p&gt;
&lt;p&gt;Anthropic’s guidance for long documents is to have the model pull out word for word quotes first, give a quote for each claim, and take back a claim when it cannot find one.&lt;span class=&quot;sn-ref&quot; id=&quot;user-content-fnref-3&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;sidenote&quot; role=&quot;note&quot;&gt;&lt;span class=&quot;sn-num&quot;&gt;3&lt;/span&gt;Anthropic, “Reduce hallucinations”. &lt;/span&gt; For a technical document I would write it like this.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;For every number you report from this document, give the page,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;the table or figure it comes from, and the sentence or table row&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;quoted word for word. If you cannot find a quote, say so and do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;not report the number.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;section data-footnotes=&quot;&quot; class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Anthropic, “PDF support”. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Liu et al. (2023), abstract. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Anthropic, “Reduce hallucinations”. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
</feed>
