<?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: Claire Dubois</title>
    <description>The latest articles on DEV Community by Claire Dubois (@claire_dubois).</description>
    <link>https://dev.to/claire_dubois</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3864020%2Fbdb7b70a-fc3f-4c16-9911-856d000ab374.jpg</url>
      <title>DEV Community: Claire Dubois</title>
      <link>https://dev.to/claire_dubois</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/claire_dubois"/>
    <language>en</language>
    <item>
      <title>Why Model Upgrades in Causal Inference Pipelines Are Never Just a Pricing Decision</title>
      <dc:creator>Claire Dubois</dc:creator>
      <pubDate>Tue, 14 Apr 2026 13:00:28 +0000</pubDate>
      <link>https://dev.to/claire_dubois/why-model-upgrades-in-causal-inference-pipelines-are-never-just-a-pricing-decision-2bn1</link>
      <guid>https://dev.to/claire_dubois/why-model-upgrades-in-causal-inference-pipelines-are-never-just-a-pricing-decision-2bn1</guid>
      <description>&lt;p&gt;&lt;strong&gt;Last month we decided to take advantage of Anthropic’s 67% price reduction on Opus 4.6 and its new 1M context window.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;On paper, the move looked straightforward: lower marginal cost, stronger reasoning on long-horizon causal tasks, same 5/25 token pricing as the previous generation. A clear win for any data science team watching burn rate.&lt;/p&gt;

&lt;p&gt;The complication appeared the moment we reran our established evaluation suite.&lt;/p&gt;

&lt;p&gt;Even with temperature fixed at 0 and identical system prompts, the new model shifted our primary causal risk difference estimates by &lt;strong&gt;0.12 to 0.19 percentage points&lt;/strong&gt; across key subgroups. More concerning, bootstrap confidence interval widths increased by &lt;strong&gt;23%&lt;/strong&gt; on protected attribute cohorts. What felt like “better intelligence” was silently altering the statistical conclusions we had been using for fairness audits and product decisions.&lt;/p&gt;

&lt;p&gt;Then came the April 6–7 Claude outages. The failure mode was not clean refusals or 503s. We received partial JSON responses that passed initial schema validation but contained truncated reasoning traces. Because longer treatment prompts (average &lt;strong&gt;4.2k&lt;/strong&gt; input tokens) were more likely to degrade, our missing-at-random assumption broke. This introduced a measurable selection bias that inflated apparent treatment lift by roughly &lt;strong&gt;8.7%&lt;/strong&gt; in the affected cohorts before detection.&lt;/p&gt;

&lt;p&gt;Replaying &lt;strong&gt;3,412&lt;/strong&gt; logged requests against a fallback provider and correcting the downstream datasets took the better part of two days. The config change to switch models? Under an hour. The statistical cleanup? Still not fully closed three weeks later.&lt;/p&gt;

&lt;p&gt;Around the same time, &lt;strong&gt;GLM-5.1&lt;/strong&gt; dropped on April 7 under MIT license; a 744B MoE model showing strong performance on long-horizon agentic tasks. We routed a subset of our autonomous causal discovery workflows through it for a week. Completion rate rose from &lt;strong&gt;64%&lt;/strong&gt; to &lt;strong&gt;71%&lt;/strong&gt; at ~&lt;strong&gt;40%&lt;/strong&gt; lower token cost, but human review time per task jumped from &lt;strong&gt;11&lt;/strong&gt; to &lt;strong&gt;19&lt;/strong&gt; minutes due to subtle factual drifts in intermediate steps.&lt;/p&gt;

&lt;p&gt;The pattern is clear: every time the provider landscape shifts, whether through pricing, deprecation (Claude 3 Haiku retiring mid-April), or new model releases like Meta’s Muse Spark on April 8; the surface-level change is trivial. The second-order effects on eval baselines, alerting thresholds, attribution chains, and fairness metrics are not.&lt;/p&gt;

&lt;p&gt;After repeating this cycle too many times, we introduced a thin, consistent abstraction layer in front of all LLM calls. We now define stable model groups with explicit fallback rules and output normalization. One provider can change behaviour or disappear without forcing the entire statistical pipeline to re-baseline.&lt;/p&gt;

&lt;p&gt;(We use this one: &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;https://github.com/maximhq/bifrost&lt;/a&gt;, though LiteLLM and Portkey also handle similar routing needs.)&lt;/p&gt;

&lt;p&gt;The infrastructure change itself was five minutes. The downstream reconciliation of old baselines took most of a week, but the next provider addition or outage will not require repeating the exercise.&lt;/p&gt;

&lt;p&gt;In causal and fairness-sensitive work, model quality is only one variable. Stability of the experimental surface matters more. Treating the LLM layer as just another interchangeable dependency is an expensive illusion. A small, well-governed routing layer turns chaotic provider churn into something closer to boring, predictable infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We should have done this way earlier.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Causal inference for credit risk: why prediction alone isn't enough</title>
      <dc:creator>Claire Dubois</dc:creator>
      <pubDate>Tue, 07 Apr 2026 13:03:23 +0000</pubDate>
      <link>https://dev.to/claire_dubois/causal-inference-for-credit-risk-why-prediction-alone-isnt-enough-1j3e</link>
      <guid>https://dev.to/claire_dubois/causal-inference-for-credit-risk-why-prediction-alone-isnt-enough-1j3e</guid>
      <description>&lt;p&gt;There's a pattern I've seen repeatedly in financial ML: a model achieves excellent predictive performance — AUC above 0.80, stable on holdout — and the team ships it. Then, six months later, someone asks "but why is the model denying more applicants from this postal code?" and nobody has a good answer.&lt;/p&gt;

&lt;p&gt;Prediction and causation are different things, and conflating them is expensive in credit risk specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core issue
&lt;/h2&gt;

&lt;p&gt;When you train a credit risk model, you're typically predicting P(default | features). This is a conditional probability — it tells you what tends to be true about people who look like this applicant. It doesn't tell you what &lt;em&gt;caused&lt;/em&gt; their credit behavior, and it doesn't tell you what &lt;em&gt;will happen&lt;/em&gt; if you lend to them.&lt;/p&gt;

&lt;p&gt;This distinction matters for two reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, selection bias.&lt;/strong&gt; Your training data only contains outcomes for people who were previously approved for credit. The people who were denied — perhaps by a prior model or manual policy — have no observed outcome. Your model is learning from a censored dataset, and it will systematically underestimate creditworthiness for groups that historical policies excluded. This is a causal problem masquerading as a data problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, feature confounding.&lt;/strong&gt; A feature that predicts default might do so because it's a proxy for the thing that actually causes default, not because of any direct relationship. If you act on that feature — use it to set rates or deny applications — you can create feedback loops that make the proxy worse over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small worked example
&lt;/h2&gt;

&lt;p&gt;Suppose you're building a model and you notice that applicants with shorter employment tenure have higher default rates. You might add employment tenure as a feature. But is tenure &lt;em&gt;causing&lt;/em&gt; default? Or is it correlated with income stability, which is correlated with the actual causal factors?&lt;/p&gt;

&lt;p&gt;In causal notation using a simple DAG:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Income Stability → Employment Tenure
Income Stability → Default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this is the true structure, conditioning on tenure is conditioning on a mediator of income stability — not on a cause of default. Adding it might actually hurt generalization if the tenure-income-stability relationship changes across different applicant populations (which it does: gig economy workers, self-employed people, recent graduates).&lt;/p&gt;

&lt;p&gt;You can sketch this in Python using the &lt;code&gt;pgmpy&lt;/code&gt; library:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pgmpy.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BayesianNetwork&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pgmpy.factors.discrete&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TabularCPD&lt;/span&gt;

&lt;span class="c1"&gt;# Define the causal structure
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BayesianNetwork&lt;/span&gt;&lt;span class="p"&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;IncomeStability&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;EmploymentTenure&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&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;IncomeStability&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;Default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# This structure tells you: controlling for IncomeStability,
# EmploymentTenure is independent of Default.
# If you can't observe IncomeStability directly, tenure is a noisy proxy —
# useful, but not causal.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is toy-level, but the point is that before you add a feature, it's worth asking: what does the causal graph look like here? Where does this variable sit in it?&lt;/p&gt;

&lt;h2&gt;
  
  
  Counterfactual thinking for policy decisions
&lt;/h2&gt;

&lt;p&gt;The more practically important application of causal thinking in credit is when you're setting policy rather than just predicting outcomes.&lt;/p&gt;

&lt;p&gt;Suppose your model predicts that applicant A has a 12% probability of default. Should you approve them? At what interest rate? The answer depends not just on the predicted probability but on what &lt;em&gt;would happen&lt;/em&gt; if you approved them vs. denied them — a counterfactual question.&lt;/p&gt;

&lt;p&gt;Difference-in-differences can help here if you have policy variation. For instance, if your institution ran a pilot program that approved a random subset of borderline applicants, you can estimate:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="c1"&gt;# Assume df has columns: pilot (bool), approved (bool), defaulted (bool)
# pilot == True means the applicant was in the random approval pilot
&lt;/span&gt;
&lt;span class="n"&gt;pilot_approved&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;pilot&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="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;approved&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="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;defaulted&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="n"&gt;control_approved&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;pilot&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="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;approved&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="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;defaulted&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;# The naive comparison is biased — control group approved via selection model
# DiD removes the selection bias if pilot assignment was truly random
&lt;/span&gt;
&lt;span class="c1"&gt;# More carefully:
&lt;/span&gt;&lt;span class="n"&gt;pilot_group&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;pilot&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="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;defaulted&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="n"&gt;control_group&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;pilot&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="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;defaulted&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;# This is the actual causal effect of approval on default rate
&lt;/span&gt;&lt;span class="n"&gt;did_estimate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pilot_group&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;control_group&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Causal effect of approval on default rate: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;did_estimate&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is only valid under the parallel trends assumption and proper randomization, but the point is that this kind of analysis tells you something a predictive model can't: what the policy &lt;em&gt;does&lt;/em&gt;, not just what it predicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fairness is a causal question
&lt;/h2&gt;

&lt;p&gt;I want to say this plainly because I see it get glossed over: fairness in credit models is not a matter of removing demographic variables from your feature set.&lt;/p&gt;

&lt;p&gt;If the causal structure includes a path from race or gender to default that runs &lt;em&gt;through&lt;/em&gt; structural inequity (lower access to credit history, discrimination in employment, etc.), then your model is going to pick up that relationship through proxies — zip code, credit utilization patterns, employment tenure — regardless of whether you included race explicitly.&lt;/p&gt;

&lt;p&gt;Equalized odds and demographic parity are useful measures, but they're metrics on your model's output, not fixes for the underlying structural problem. You can satisfy equalized odds and still be making decisions that replicate the effects of discrimination via features correlated with protected attributes.&lt;/p&gt;

&lt;p&gt;What actually helps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drawing out the causal graph for your features and understanding which paths you want to block&lt;/li&gt;
&lt;li&gt;Orthogonalization: regressing out protected attribute variation from proxy features before training (double ML approach)&lt;/li&gt;
&lt;li&gt;Testing calibration &lt;em&gt;across groups&lt;/em&gt;, not just aggregate calibration
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.calibration&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;calibration_curve&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;

&lt;span class="n"&gt;groups&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;protected_group&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subplots&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;subset&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;protected_group&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;group&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;fraction_pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mean_pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calibration_curve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;defaulted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
        &lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;predicted_prob&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
        &lt;span class="n"&gt;n_bins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fraction_pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Group: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plot&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;k--&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Perfect calibration&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;legend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_xlabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Mean predicted probability&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_ylabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Fraction positive&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Calibration by group&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tight_layout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A model that's miscalibrated for one group — even if aggregate calibration looks fine — is making systematically wrong decisions for that group.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually recommend
&lt;/h2&gt;

&lt;p&gt;This is not "you must learn do-calculus before you're allowed to build models." That's not realistic or helpful. But a few concrete things make a material difference:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sketch the causal graph for your most important features.&lt;/strong&gt; Even informally. Ask: is this a cause, a proxy, or a consequence of the thing I'm trying to predict?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check whether your training data is censored&lt;/strong&gt; and in which direction. If prior decisions affect who appears in your training set, your model is not learning from a representative sample.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Separate your prediction task from your policy decision.&lt;/strong&gt; The model gives you a probability. The policy is what you do with it. Causal thinking belongs in the policy layer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Calibrate across subgroups, always.&lt;/strong&gt; Aggregate calibration is necessary but not sufficient.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Prediction is a tool. It's a useful one. But in high-stakes decisions — credit, healthcare, hiring — prediction without causal reasoning is how you build systems that perform well on metrics and cause harm in the world.&lt;/p&gt;

&lt;p&gt;Enfin. That's the argument I make internally at least twice a month, so I figured I'd write it down properly.&lt;/p&gt;

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