<?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: Jason Lau</title>
    <description>The latest articles on DEV Community by Jason Lau (@jasonl888).</description>
    <link>https://dev.to/jasonl888</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%2F4040954%2F51c880e9-8eb1-4df8-8b79-d4ca3b884552.jpg</url>
      <title>DEV Community: Jason Lau</title>
      <link>https://dev.to/jasonl888</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jasonl888"/>
    <language>en</language>
    <item>
      <title>Your AI's EDA Looks Right. Here's How to Tell When It Isn't.</title>
      <dc:creator>Jason Lau</dc:creator>
      <pubDate>Wed, 22 Jul 2026 02:51:22 +0000</pubDate>
      <link>https://dev.to/jasonl888/your-ais-eda-looks-right-heres-how-to-tell-when-it-isnt-4826</link>
      <guid>https://dev.to/jasonl888/your-ais-eda-looks-right-heres-how-to-tell-when-it-isnt-4826</guid>
      <description>&lt;p&gt;Ask an AI coding assistant to run exploratory data analysis on a new dataset and it will produce, in seconds, something that looks like a competent analyst's first pass: &lt;code&gt;.describe()&lt;/code&gt;, a correlation matrix, some distribution plots, a groupby or two, and a paragraph of narrative summarizing what it found.&lt;/p&gt;

&lt;p&gt;The problem is not that this output is wrong. Often it's correct. The problem is that &lt;strong&gt;it looks identical whether it's correct or not&lt;/strong&gt; — and the failure modes that make it wrong are exactly the ones a quick skim won't catch, because nothing throws an error and nothing looks incomplete.&lt;/p&gt;

&lt;p&gt;Here are six patterns that show up constantly in AI-generated EDA, each one silently producing output that reads as clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Profiling before type conversion
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The AI commonly generates this order — the problem is invisible in the output
&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;describe&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                                    &lt;span class="c1"&gt;# order_date excluded — it's still a string
&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;order_date&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;order_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;     &lt;span class="c1"&gt;# too late
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.describe()&lt;/code&gt; silently drops non-numeric columns. If a date column is still sitting as a string object when you profile, it just... isn't in the summary. Nothing errors. You get a clean-looking table with one fewer column than you meant to check, and no indication anything was skipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is ordering, not code complexity&lt;/strong&gt;: type conversions belong in the very first block, before any statistic is computed.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Reaching for &lt;code&gt;.mean()&lt;/code&gt; on a right-skewed column
&lt;/h2&gt;

&lt;p&gt;AI assistants default to &lt;code&gt;.mean()&lt;/code&gt; for basically every "typical value" question, because it's the statistically obvious first move. For revenue, spend, session duration — anything with a long right tail — that default quietly overstates what's typical:&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="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&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;revenue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# a handful of large B2B orders pull this up
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A groupby that reports mean without also reporting median, count, and std produces a number that's technically correct and practically misleading. The fix is cheap — always pull all four together — but it requires knowing to ask, since the AI won't flag its own choice of statistic as a judgment call.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;.dropna()&lt;/code&gt; before you've profiled the missingness
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&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;dropna&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# applied early, "to make analysis cleaner"
# profiling happens on what's left
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Profiling exists to characterize &lt;em&gt;how&lt;/em&gt; and &lt;em&gt;where&lt;/em&gt; values are missing — whether it's random, or concentrated in a specific segment, or correlated with another column. Drop the rows first and that pattern is gone before you ever looked at it. The output afterward looks complete. It's complete for a dataset that no longer represents the one you started with.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The silent subset filter
&lt;/h2&gt;

&lt;p&gt;This is the one most likely to change your conclusions without you noticing:&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="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="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;status&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="c1"&gt;# ← added mid-script, nothing downstream is labeled
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every correlation, every groupby, every chart that runs after this line applies to delivered orders only — but nothing in the output says so. A stakeholder reading "42% of orders come from the enterprise channel" has no way to know that's 42% &lt;em&gt;of delivered orders&lt;/em&gt;, unless someone happened to scroll up to a filter line with no comment attached to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Narrative summaries that quietly assert causation
&lt;/h2&gt;

&lt;p&gt;AI-generated EDA often ends with a paragraph of plain-language commentary. That commentary is where causal language creeps in — "unit price drives revenue," "the strong correlation confirms X" — from analysis that only ever established correlation. The numbers above the paragraph are usually fine. The prose interpreting them is where scope creep happens, because narrative generation and statistical rigor are not the same task, even when the same model produces both in one response.&lt;/p&gt;

&lt;p&gt;Read the numbers. Treat the narrative paragraph as a first draft to revise, not a finding to cite.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Treating "no issues found" as "the data is clean"
&lt;/h2&gt;

&lt;p&gt;If you ask an AI to check for data quality issues and it reports none, that's not confirmation. It means the checks it ran didn't find anything — which says nothing about the checks it didn't run. Cardinality on string columns, structural missingness patterns, multivariate outliers: these don't show up unless something specifically goes looking for them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe236hz0odhr489jv1joz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe236hz0odhr489jv1joz.png" alt="Six failure patterns in two columns: Profile before type conversion (date columns silently absent), Mean for skewed columns (typical value overstated), dropna before profiling (missingness pattern lost), Silent subset filter (all subsequent analysis scoped without a label), Accept narrative without reading numbers (causal claims go unchallenged), No findings equals clean data (unchecked assumptions pass through)" width="800" height="414"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;All six patterns share the same shape: the output looks complete, and completeness is exactly what a quick review checks for.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just review the code" isn't quite the answer
&lt;/h2&gt;

&lt;p&gt;Reviewing AI-generated code before running it catches some of this — type conversions before statistics, mean vs. median, subset filters — because those are visible in the script itself. What code review alone won't catch is #5 and #6: causal language buried in a narrative summary, and the false confidence of a clean-looking "no issues found." Those require reading the &lt;em&gt;output&lt;/em&gt;, not just the code, and holding it against what you already know about the domain.&lt;/p&gt;

&lt;p&gt;A defensible EDA — AI-assisted or not — means you can answer yes to all of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type conversions happened before any statistic was computed&lt;/li&gt;
&lt;li&gt;Missingness was profiled before anything was dropped&lt;/li&gt;
&lt;li&gt;Every group summary includes count and spread, not just mean&lt;/li&gt;
&lt;li&gt;No subset filter changed scope without being documented&lt;/li&gt;
&lt;li&gt;Causal language has been replaced with correlational language where causation wasn't established&lt;/li&gt;
&lt;li&gt;"No findings" was treated as "the checks I ran found nothing," not as "the data is clean"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is really about AI. It's the same discipline that's always separated a defensible analysis from a plausible-looking one. What's changed is the speed at which plausible-looking output gets produced — which means the checklist matters more, not less.&lt;/p&gt;




&lt;p&gt;This is the framework behind &lt;a href="https://lms.sophiarch.com/courses/exploratory-data-analysis?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=ai-eda-failure-patterns" rel="noopener noreferrer"&gt;SophiArch's Exploratory Data Analysis course&lt;/a&gt;, including a lab where you're handed a full AI-generated EDA notebook and asked to find exactly these kinds of issues before they reach a stakeholder.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>python</category>
      <category>pandas</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
