<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: john</title>
    <description>The latest articles on DEV Community by john (@john_xbrr).</description>
    <link>https://dev.to/john_xbrr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4072639%2F83538b20-99de-4093-9866-d82333f2719b.png</url>
      <title>DEV Community: john</title>
      <link>https://dev.to/john_xbrr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/john_xbrr"/>
    <language>en</language>
    <item>
      <title>What AI actually speeds up in research work</title>
      <dc:creator>john</dc:creator>
      <pubDate>Tue, 11 Aug 2026 08:42:49 +0000</pubDate>
      <link>https://dev.to/john_xbrr/what-ai-actually-speeds-up-in-research-work-4h2m</link>
      <guid>https://dev.to/john_xbrr/what-ai-actually-speeds-up-in-research-work-4h2m</guid>
      <description>&lt;p&gt;I've been keeping a rough log for about a year of where AI tools saved me time in day-to-day research work, and where they quietly cost me time. The results were not what I expected. The wins were almost all in boring places. The losses were all in the places that felt most impressive in the demo.&lt;/p&gt;

&lt;p&gt;There's one rule that predicts the split:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI is worth using wherever verifying the output is cheaper than producing it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's it. Everything below follows from that. Writing a pandas transform is slow; checking whether a transform did the right thing is fast, because you can look at the output. Reasoning about whether a mixed-effects model is correctly specified is slow, and checking someone else's reasoning about it is just as slow. So the first one is a great use and the second one is a trap.&lt;/p&gt;

&lt;p&gt;Here's the log.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Cleaning data files nobody designed
&lt;/h2&gt;

&lt;p&gt;Every lab has a machine that exports CSVs written by someone in 2011. Units in the header row. Three date formats in one column because the firmware was updated halfway through the study. A stray &lt;code&gt;n.d.&lt;/code&gt; where a float should be. Column names with trailing spaces that you will not see until you spend forty minutes on a &lt;code&gt;KeyError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The thing that made this work well was a change in what I ask for. I don't hand the model the data and ask for clean data back. I hand it a &lt;em&gt;sample&lt;/em&gt; and ask for the transform.&lt;/p&gt;

&lt;p&gt;What I paste is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtypes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isna&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus a plain description of what's wrong and what the target schema is. What comes back is a function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# instrument switched date format at firmware 2.4
&lt;/span&gt;    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;collected_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;collected_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mixed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dayfirst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coerce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# "n.d." and "&amp;lt;LOD" both mean below detection, but they are not the same
&lt;/span&gt;    &lt;span class="c1"&gt;# as missing — keep the distinction in a separate flag column
&lt;/span&gt;    &lt;span class="n"&gt;below_lod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n.d.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;LOD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;below_lod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;below_lod&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;below_lod&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coerce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model never touches a single data point. It writes a transform, I read the transform, and I check it against the source file. If the transform is wrong, it's wrong loudly and identically on every row, which is exactly the failure mode you want. Silent per-row edits are the failure mode you don't.&lt;/p&gt;

&lt;p&gt;One habit that pays for itself: ask for assertions at the end of the function, then keep them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;collected_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;notna&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unparsed dates remain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;between&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e4&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isna&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They cost nothing and they've caught two genuine data problems for me that had nothing to do with AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. First-pass literature screening
&lt;/h2&gt;

&lt;p&gt;Not summarizing. Screening.&lt;/p&gt;

&lt;p&gt;If you're starting a review and you have 800 abstracts from a Semantic Scholar or PubMed query, the expensive part isn't reading the 40 relevant ones. It's finding out which 40. That's a classification job with a rubric, and it's a good fit because verification is cheap: you spot-check.&lt;/p&gt;

&lt;p&gt;The setup I use is boring on purpose. Write the inclusion criteria out explicitly, as if for a human RA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INCLUDE if the paper reports an original measurement of X in a mammalian system.
EXCLUDE reviews, simulations without experimental validation, and non-mammalian systems.
If the abstract is ambiguous about the system, mark UNSURE — do not guess.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run each abstract through and store &lt;code&gt;decision&lt;/code&gt;, &lt;code&gt;reason&lt;/code&gt;, and the abstract ID. Three things matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the UNSURE bucket.&lt;/strong&gt; A screener that is forced to make a binary call will make one, confidently. The unsure pile is where you'll actually read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log the rejections with reasons.&lt;/strong&gt; You need to be able to audit this later, and if you're writing a systematic review you need it for the methods section anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand-screen a random 50 yourself and compare.&lt;/strong&gt; This takes an hour and tells you whether the rubric is working. If agreement is bad, the rubric is ambiguous, not the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last point is the whole ballgame. Most of the time when screening goes badly it's because the criteria were never precise enough for a human RA either, and you've just found out.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Code you know how to verify but hate to write
&lt;/h2&gt;

&lt;p&gt;Argparse blocks. &lt;code&gt;matplotlib&lt;/code&gt; subplot grids where you have to remember whether it's &lt;code&gt;axes[i, j]&lt;/code&gt; or &lt;code&gt;axes[j, i]&lt;/code&gt;. &lt;code&gt;statsmodels&lt;/code&gt; formula syntax. Snakemake rules. The regex for parsing your instrument's filename convention.&lt;/p&gt;

&lt;p&gt;This is the highest-return category and the least interesting one. The common feature is that I can look at the output and know immediately whether it's right, usually by running it. Nobody needs to be careful here.&lt;/p&gt;

&lt;p&gt;The related case is the throwaway script that would otherwise not get written at all. A five-minute script that renames 400 files according to the sample sheet used to be a thing I did by hand because writing the script felt like more work. That trade has changed, and downstream it means fewer manual steps that never got documented.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Figures, which are a file format problem more than a drawing problem
&lt;/h2&gt;

&lt;p&gt;Figures are where I expected the biggest win and got the most complicated answer.&lt;/p&gt;

&lt;p&gt;The drawing is not the bottleneck. Ask anyone who has published: the bottleneck is revision three, when a reviewer asks you to rename one protein in panel B, and the layered source file is on a laptop that got reimaged, and the only surviving artifact is a flattened 300 dpi PNG. Now you're rebuilding an entire panel at 1 AM because of one word.&lt;/p&gt;

&lt;p&gt;So the useful question for any figure tool, AI or not, isn't "can it produce a nice image." It's "what does the file look like six months from now." A generated PNG that can't be edited has moved the problem, not solved it. I want layered SVG that opens properly in Illustrator or Inkscape, or an editable PPTX if that's where the group's slide deck lives.&lt;/p&gt;

&lt;p&gt;That's roughly why I use &lt;a href="https://scientificfigure.com/" rel="noopener noreferrer"&gt;Scientific Figure&lt;/a&gt; for schematics and graphical abstracts rather than a general image model: it goes from a description, a notebook sketch, or a reference figure to a draft, but the output stays as editable layers you can export as SVG or PPTX. The point isn't the generation step. It's that the revision six months later costs two minutes instead of an afternoon.&lt;/p&gt;

&lt;p&gt;Two things worth saying plainly. First, you have to check every label yourself, every time. Generated text inside images is the single least reliable thing in this whole workflow, and a transposed subscript in a figure is a correction notice. Second, check your target journal's AI disclosure policy before you submit anything, because they vary and they've been changing every few months.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Reverse-outlining your own writing
&lt;/h2&gt;

&lt;p&gt;I don't draft prose with AI. What I did find useful is the opposite direction: paste in a section you already wrote and ask for the outline it implies. One sentence per paragraph, no commentary.&lt;/p&gt;

&lt;p&gt;When the returned outline doesn't match the outline you had in your head, that's a real finding about your writing. Usually it means paragraph four is doing two jobs, or the argument you thought was in the discussion is actually only in the figure caption.&lt;/p&gt;

&lt;p&gt;The adversarial version works too: "You are reviewer 2. List the three methodological objections most likely to appear in a review of this section." You'll get some noise, but the objections that make you wince are the ones a real reviewer will also raise, and it's much cheaper to hear them now.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Things it cost me time on
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Statistics reasoning.&lt;/strong&gt; Ask about model specification, multiple comparison correction, or what a particular p-value licenses you to claim, and you get an answer with the shape of expertise. Some of them are wrong in ways that take a domain expert to see. Verification here is as expensive as doing it yourself, so by the rule at the top, it's not worth it. I ask a statistician.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Citations.&lt;/strong&gt; This should be a solved problem and isn't. Anything that produces a reference without retrieving it will produce references that don't exist, with plausible authors, in plausible journals, in plausible years. Every citation gets resolved by DOI or it doesn't go in. No exceptions, no "it's probably fine."&lt;/p&gt;

&lt;p&gt;The pattern in both is the same: the output is confident, structurally correct, and expensive to check. That's the exact profile of things not to automate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule again
&lt;/h2&gt;

&lt;p&gt;Sort your tasks by how expensive it is to check the answer, not by how hard the task is.&lt;/p&gt;

&lt;p&gt;Data cleaning, boilerplate, file wrangling, first-pass screening: check cheap, automate freely. Statistics, citations, anything where being subtly wrong is indistinguishable from being right until a reviewer catches it: do it yourself, or ask a human who knows.&lt;/p&gt;

&lt;p&gt;The tooling will keep getting better and this rule won't change, because it isn't about capability. It's about who's accountable for the paper.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>science</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
