<?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: InterSystems</title>
    <description>The latest articles on DEV Community by InterSystems (intersystems).</description>
    <link>https://dev.to/intersystems</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%2Forganization%2Fprofile_image%2F2450%2F5c611adb-602d-4948-b84b-5fe47046fd5c.png</url>
      <title>DEV Community: InterSystems</title>
      <link>https://dev.to/intersystems</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/intersystems"/>
    <language>en</language>
    <item>
      <title>Long Running SQL Queries: a sample exploration</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Fri, 24 Jul 2026 15:59:40 +0000</pubDate>
      <link>https://dev.to/intersystems/long-running-sql-queries-a-sample-exploration-34b4</link>
      <guid>https://dev.to/intersystems/long-running-sql-queries-a-sample-exploration-34b4</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Here at InterSystems, we often deal with massive datasets of structured data. It’s not uncommon to see customers with tables spanning &amp;gt;100 fields and &amp;gt;1 billion rows, each table totaling hundred of GB of data. Now imagine joining two or three of these tables together, with a schema that wasn’t optimized for this specific use case. Just for fun, let’s say you have 10 years worth of EMR data from 20 different hospitals across your state, and you’ve been tasked with finding….&lt;br&gt;&amp;nbsp; &amp;nbsp;every clinician within your network&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; who has administered a specific drug&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; between the years of 2017-2019&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp;to patients who reside outside the state&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;and have one of the following conditions [diabetes, hypertension, asthma]&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; where the cost was covered by Medicaid&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;I’ve seen our technology handle &lt;a href="https://www.intersystems.com/success-stories/a-billion-data-points-for-innovations-in-care-and-care-coordination/" rel="noopener noreferrer"&gt;these sort of cases just fine&lt;/a&gt;, but the query may still take a while to run. Can it be faster though? Let me walk you through a sample investigation. &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;///////////////////////////////////////////////////////////////////////////////////////////////&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;strong&gt;The Need:&lt;/strong&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Find all patients who have had an outpatient encounter at a facility located in one of these counties in the year 2022 or 2023&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;strong&gt;The Query:&lt;/strong&gt;&lt;br&gt;SELECT DISTINCT enc.Patient-&amp;gt;PatientNumber &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;FROM EMR.Encounter as enc&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;INNER JOIN State_Facility.Address as fa on enc.Facility = fa.FacilityCode&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;INNER JOIN State_Geography.Cities as city ON city.Zip = fa.ZipCode&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;WHERE enc.EncounterTime BETWEEN '2022-01-01' AND '2023-12-31'&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;AND enc.EncounterType IN ('OP','Outpatient','O')&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;AND city.County IN ('Los Angeles County', 'Orange County', 'Riverside County', 'San Bernardino County', 'Ventura County')&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;strong&gt;The Performance:&lt;/strong&gt;&lt;br&gt;The query was taking &amp;gt;24 hours to complete&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;INVESTIGATION STEPS:&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;1) Review the tables that you’re querying. What relationships or foreign keys exist between them? What indices already exist? Is your SQL query making good use of the ones that already exist? Do the indices have Status = Selectable?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;We checked each field that was part of a WHERE, AND, or INNER JOIN. Most of them did have indices, including some bitmap indices. [NOTE: Further to the right of the screenshot page, the Status column shows that EncounterTypeIndex is Selectable]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&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%2Fceqh5hpf4ydnwx2310b8.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%2Fceqh5hpf4ydnwx2310b8.png" alt=" " width="800" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2) Review the Query Plan. Does it make sense? Does it make use of the indices and relationships you expected it would? If not, does it seem more or less efficient?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Yes, the Query Plan showed effective use of the indices on EncounterType and StartTime. [NOTE: This screenshot is for a simplified version of the query that does not consider the zip code of the encounter facility]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&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%2Fzskc4vk2isx8kchxlq6q.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%2Fzskc4vk2isx8kchxlq6q.png" alt=" " width="799" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;3) Ensure the table statistics up to date by running &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=GSOD_opttable" rel="noopener noreferrer"&gt;Tune Tables&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;strong&gt;4) Check whether the actual Query Plan at runtime matches the one you were shown. &lt;/strong&gt;The "Show Plan" Query Plan does not utilize the &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSOC_rtpc" rel="noopener noreferrer"&gt;Runtime Plan Choice (RTPC)&lt;/a&gt; optimization when it generates a Query Plan, but the RTPC is utilized when the query is actually run. That is why the Show Plan Query Plan and the runtime Query Plan can be different. The RTPC algorithm usually finds an optimal choice, but it can sometimes make a poor choice. If we find that the RTPC algorithm is making the wrong choice, it is possible to suppress the RTPC at runtime by using the &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_select#RSQL_select_args_keyword" rel="noopener noreferrer"&gt;%NORUNTIME keyword&lt;/a&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Once the query was running, we looked at the Processes page and found the process that was running the query. We found the cached query that it was running (the Routine). We went to that cached query and looked at its Query Plan. We found that it was using a Query Plan that was very different from the one we’d seem before, and it looked much less efficient.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&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%2Fut51eitny82zkwr1pcy3.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%2Fut51eitny82zkwr1pcy3.png" alt=" " width="799" height="452"&gt;&lt;/a&gt;&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%2Fs951cou8nur54mt4rmzp.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%2Fs951cou8nur54mt4rmzp.png" alt=" " width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;RECOMMENDATIONS:&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;We recommended that the customer take the following actions:&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;1) Use the &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_select#RSQL_select_args_keyword" rel="noopener noreferrer"&gt;%NORUNTIME keyword&lt;/a&gt; when executing the query, forcing it to use the more efficient Query Plan&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2) Build a new bitmap index called EncounterDate based on the EncounterTime field. Date-based indices can be faster than DateTime-based indices, and bitmap indices are often significantly faster than normal indices&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Once they implemented these two recommendations, their query was now completing in ~6 hours, a 75% improvement.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;FURTHER READING:&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check out &lt;a class="mentioned-user" href="https://dev.to/benjamin"&gt;@benjamin&lt;/a&gt;.Spead's excellent collection of resources, which includes links to&amp;nbsp;online documentation, InterSystems online learning courses, presentation slideshows, and Developer Community articles.&lt;br&gt;&lt;a href="https://community.intersystems.com/post/sql-performance-resources" rel="noopener noreferrer"&gt;https://community.intersystems.com/post/sql-performance-resources&lt;/a&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>performance</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Monitoring InterSystems IRIS with Prometheus and Grafana</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Wed, 22 Jul 2026 17:17:48 +0000</pubDate>
      <link>https://dev.to/intersystems/monitoring-intersystems-iris-with-prometheus-and-grafana-37hh</link>
      <guid>https://dev.to/intersystems/monitoring-intersystems-iris-with-prometheus-and-grafana-37hh</guid>
      <description>&lt;p&gt;Monitoring your IRIS deployment is crucial. With the deprecation of&amp;nbsp;&lt;strong&gt;System Alert and Monitoring (SAM),&lt;/strong&gt;&amp;nbsp;a modern, scalable solution is necessary for&amp;nbsp;&lt;strong&gt;real-time insights, early issue detection, and operational efficiency.&lt;/strong&gt;&amp;nbsp;This guide covers setting up&amp;nbsp;&lt;strong&gt;Prometheus and Grafana&lt;/strong&gt;&amp;nbsp;in Kubernetes to monitor&amp;nbsp;&lt;strong&gt;InterSystems IRIS&amp;nbsp;&lt;/strong&gt;effectively.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;This guide assumes you already have an IRIS cluster deployed using the InterSystems Kubernetes Operator (IKO), which simplifies deployment, integration and mangement.&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%2Fwn8q07hsv4ubuvwxe6fl.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%2Fwn8q07hsv4ubuvwxe6fl.png" alt=" " width="800" height="368"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;&lt;strong&gt;Why Prometheus and Grafana?&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prometheus&lt;/strong&gt; and &lt;strong&gt;Grafana&lt;/strong&gt; are widely adopted tools for cloud-native monitoring and visualization. Here’s why they are a fit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalability:&lt;/strong&gt; Prometheus handles large-scale data ingestion efficiently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting:&lt;/strong&gt; Customizable alerts via &lt;strong&gt;Prometheus Alertmanager.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visualization:&lt;/strong&gt; Grafana offers rich, customizable dashboards for Kubernetes metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ease of Integration:&lt;/strong&gt; Seamlessly integrates with &lt;strong&gt;Kubernetes workloads.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;Before starting, ensure you have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic knowledge of Kubernetes and Linux&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubectl&lt;/code&gt; and &lt;code&gt;helm&lt;/code&gt; installed.&lt;/li&gt;
&lt;li&gt;Familiarity with Prometheus concepts (refer to the &lt;a href="https://prometheus.io/docs/introduction/overview/" rel="noopener noreferrer"&gt;Prometheus documantion&lt;/a&gt; for more information).&lt;/li&gt;
&lt;li&gt;A deployed IRIS instance using the &lt;strong&gt;InterSystems Kubernetes Operator (IKO)&lt;/strong&gt;, refer to another article &lt;a href="https://docs.intersystems.com/components/csp/docbook/DocBook.UI.Page.cls?KEY=AIKO" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Step 1: Enable Metrics in InterSystems IRIS&lt;/h2&gt;

&lt;p&gt;InterSystems IRIS exposes metrics via &lt;code&gt;/api/monitor/&lt;/code&gt;&amp;nbsp;in the Prometheus format. Ensure this endpoint is enabled:&lt;/p&gt;

&lt;ol start="1"&gt;
&lt;li&gt;Open the Management Portal.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;System Administration &amp;gt; Security &amp;gt; Applications &amp;gt; Web Applications&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Ensure &lt;code&gt;/api/monitor/&lt;/code&gt; is enabled and accessible by Prometheus. You can check its status by navigating to the Management Portal, going to &lt;strong&gt;System Administration &amp;gt; Security &amp;gt; Applications &amp;gt; Web Applications&lt;/strong&gt;, and verifying that the endpoint is listed and enabled.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Verify its availability by accessing:&lt;/p&gt;

&lt;pre&gt;http://&amp;lt;IRIS_HOST&amp;gt;:&amp;lt;PORT&amp;gt;/api/monitor/metrics&lt;/pre&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%2F4xrra3jgxqgg2420kviw.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%2F4xrra3jgxqgg2420kviw.png" alt=" " width="800" height="333"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;&lt;strong&gt;Step 2: Deploy Prometheus Using Helm&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;Deploying Prometheus using &lt;strong&gt;Helm&lt;/strong&gt; provides an easy-to-manage monitoring setup. We will use the &lt;code&gt;kube-prometheus-stack&lt;/code&gt; chart that includes Prometheus, Alertmanager, and Grafana.&lt;/p&gt;

&lt;ol start="1"&gt;
&lt;li&gt;
&lt;strong&gt;Prepare the configuration:&lt;/strong&gt; Create a &lt;code&gt;values.yaml&lt;/code&gt; file with the following settings:
&lt;pre&gt;prometheus:
&amp;nbsp; prometheusSpec:
&amp;nbsp; &amp;nbsp; additionalScrapeConfigs:
&amp;nbsp; &amp;nbsp; &amp;nbsp; - job_name: 'intersystems_iris_metrics'
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; metrics_path: '/api/monitor/metrics'
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; static_configs:
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - targets:
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 'iris-app-compute-0.iris-svc.commerce.svc.cluster.local:80' # Replace with your IRIS service

      # To scrape custom metrics from the REST API created in IRIS
      - job_name: 'custom_iris_metrics'
        metrics_path: '/web/metrics'
        static_configs:
          - targets:
              - 'commerce-app-webgateway-0.iris-svc.commerce.svc.cluster.local:80'
        basic_auth:
          username: '_SYSTEM'
          password: 'SYS'&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explanation:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;iris-app-compute-0.iris-svc.commerce.svc.cluster.local:80&lt;/strong&gt;&lt;/code&gt;: The format of the target should follow this convention: &lt;code&gt;&amp;lt;pod-name&amp;gt;-iris-svc.&amp;lt;namespace&amp;gt;.svc.cluster.local:80&lt;/code&gt;. Replace &lt;code&gt;&amp;lt;pod-name&amp;gt;&lt;/code&gt;&amp;nbsp;with your IRIS pod, specify whether you want to scrape &lt;code&gt;compute&lt;/code&gt; or &lt;code&gt;data&lt;/code&gt; pods, and adjust the namespace as needed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;basic_auth&lt;/strong&gt;&lt;/code&gt;** section**: If authentication is required to access the IRIS metrics endpoint, provide the necessary credentials.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the Helm repository:&lt;/strong&gt;
&lt;pre&gt;helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update&lt;/pre&gt;
&lt;/li&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/uyfgv31f7g9hkvfegxoe.png) &amp;nbsp;
&lt;li&gt;
&lt;strong&gt;Install Prometheus using Helm:&lt;/strong&gt;
&lt;pre&gt;helm install monitoring prometheus-community/kube-prometheus-stack -n monitoring --create-namespace -f values.yaml&lt;/pre&gt;
&lt;/li&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/sktubx1bti39hvr574la.png)
&lt;li&gt;
&lt;strong&gt;Verify the deployment:&lt;/strong&gt;
&lt;pre&gt;kubectl get pods -n monitoring&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&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%2F5gw71yjc32t0091v91p4.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%2F5gw71yjc32t0091v91p4.png" alt=" " width="666" height="194"&gt;&lt;/a&gt; &amp;nbsp;&lt;/p&gt;





&lt;h2&gt;Step 3: Custom Metrics with REST API&lt;/h2&gt;

&lt;p&gt;You can create a&lt;strong&gt; custom metrics CSP&lt;/strong&gt; page that serves your application metrics. In this guide, I provide an example of a simple CSP page that extracts system metrics from IRIS itself, but you can totally build your own CSP page with your own custom metrics—just make sure they are in the &lt;strong&gt;Prometheus format.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;br&gt;
CustomMetrics.REST&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="hljs-keyword"&gt;Class&lt;/span&gt;&amp;nbsp;CustomMetrics.REST&amp;nbsp;&lt;span class="hljs-keyword"&gt;Extends&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;%CSP.REST&lt;/span&gt;
{ &lt;span class="hljs-keyword"&gt;Parameter&lt;/span&gt;&amp;nbsp;HandleCorsRequest = &lt;span class="hljs-number"&gt;1&lt;/span&gt;&lt;span class="hljs-comment"&gt;; ClassMethod&amp;nbsp;Metrics() As&amp;nbsp;%Status&lt;/span&gt;
{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Try&lt;/span&gt;&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Do&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;%response.SetHeader&lt;/span&gt;(&lt;span class="hljs-string"&gt;"Content-Type"&lt;/span&gt;, &lt;span class="hljs-string"&gt;"text/plain; version=0.0.4; charset=utf-8"&lt;/span&gt;)
 &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;New&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;$Namespace&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Set&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;$Namespace&lt;/span&gt;&amp;nbsp;= &lt;span class="hljs-string"&gt;"%SYS"&lt;/span&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Set&lt;/span&gt;&amp;nbsp;ref&amp;nbsp;= &lt;span class="hljs-keyword"&gt;##class&lt;/span&gt;(SYS.Stats.Dashboard).Sample()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# HELP iris_license_high Peak number of licenses used"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# TYPE iris_license_high gauge"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"iris_license_high "&lt;/span&gt;, ref.LicenseHigh, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# HELP iris_active_processes Number of active processes"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# TYPE iris_active_processes gauge"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"iris_active_processes "&lt;/span&gt;, ref.Processes, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# HELP iris_application_errors Number of application errors"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# TYPE iris_application_errors counter"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"iris_application_errors "&lt;/span&gt;, ref.ApplicationErrors, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Return&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;$$$OK&lt;/span&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&amp;nbsp;&lt;span class="hljs-keyword"&gt;Catch&lt;/span&gt;&amp;nbsp;ex&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Do&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;%response.SetHeader&lt;/span&gt;(&lt;span class="hljs-string"&gt;"Content-Type"&lt;/span&gt;, &lt;span class="hljs-string"&gt;"text/plain"&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"Internal Server Error"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Do&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;$System&lt;/span&gt;.Status.DisplayError(ex.AsStatus())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Return&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;$$$ERROR&lt;/span&gt;(&lt;span class="hljs-built_in"&gt;$$$GeneralError&lt;/span&gt;, &lt;span class="hljs-string"&gt;"Internal Server Error"&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
} 
XData&amp;nbsp;UrlMap
{
&amp;lt;Routes&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;Route&amp;nbsp;Url=&lt;span class="hljs-string"&gt;"/metrics"&lt;/span&gt;&amp;nbsp;Method=&lt;span class="hljs-string"&gt;"GET"&lt;/span&gt;&amp;nbsp;Call=&lt;span class="hljs-string"&gt;"Metrics"&lt;/span&gt;/&amp;gt;
&amp;lt;/Routes&amp;gt;
} 
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;p&gt;Deploy this as a REST service under a new web application called &lt;code&gt;&lt;strong&gt;metrics&lt;/strong&gt;&lt;/code&gt; in IRIS, and add its path to Prometheus for scraping.&lt;/p&gt;





&lt;h2&gt;Step 4: Verify Prometheus Setup&lt;/h2&gt;

&lt;ol start="1"&gt;
&lt;li&gt;Open the Prometheus UI (&lt;code&gt;http://&amp;lt;PROMETHEUS_HOST&amp;gt;:9090&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Status &amp;gt; Targets&lt;/strong&gt; and confirm IRIS metrics are being scraped.&lt;/li&gt;
&lt;/ol&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%2Fh5jutrsrfns2sl4w8z0w.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%2Fh5jutrsrfns2sl4w8z0w.png" alt=" " width="800" height="209"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h3&gt;Step 5: Access Grafana&lt;/h3&gt;

&lt;p&gt;With Prometheus scraping IRIS metrics, the next step is to visualize the data using Grafana.&lt;/p&gt;

&lt;p&gt;1.&amp;nbsp;&lt;strong&gt;Retrieve the Grafana service details:&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl get svc -n monitoring&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you’re using an ingress controller, you can access Grafana using the configured hostname (e.g., &lt;code&gt;http://grafana.example.com&lt;/code&gt;). Otherwise, you can use the following options:&lt;/p&gt;

&lt;ol start="1"&gt;
&lt;li&gt;
&lt;strong&gt;Port Forwarding&lt;/strong&gt;: Use &lt;code&gt;kubectl port-forward&lt;/code&gt; to access Grafana locally:
&lt;pre&gt;&lt;code&gt;kubectl port-forward svc/monitoring-grafana -n monitoring 3000:80&lt;/code&gt;&lt;/pre&gt;
Then, access Grafana at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NodePort or ClusterIP&lt;/strong&gt;: Refer to the &lt;code&gt;NodePort&lt;/code&gt; or &lt;code&gt;ClusterIP&lt;/code&gt; service details from the command output to connect directly.&lt;/li&gt;
&lt;/ol&gt;





&lt;h3&gt;Step 6: Log In to Grafana&lt;/h3&gt;

&lt;p&gt;Use the default credentials to log in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Username&lt;/strong&gt;: &lt;code&gt;admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password&lt;/strong&gt;: &lt;code&gt;prom-operator&lt;/code&gt; (or the password set during installation).&lt;/li&gt;
&lt;/ul&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%2Fxp4sph9cs735o8zsrioh.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%2Fxp4sph9cs735o8zsrioh.png" alt=" " width="577" height="553"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;&lt;strong&gt;Step 7: Import a Custom Dashboard&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;I’ve created a&lt;strong&gt; custom dashboard&lt;/strong&gt; specifically tailored for &lt;strong&gt;InterSystems IRIS&lt;/strong&gt; metrics, which you can use as a starting point for your monitoring needs. The JSON file for this dashboard is hosted on GitHub for easy access and import:&amp;nbsp;&lt;a href="https://raw.githubusercontent.com/sbendarsky/monitoring-iris/refs/heads/main/grafana-dashboard.json" rel="noopener noreferrer"&gt;Download the Custom Dashboard JSON&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To import the dashboard:&lt;/p&gt;

&lt;ol start="1"&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Dashboards &amp;gt; Import&lt;/strong&gt; in Grafana.&lt;/li&gt;
&lt;li&gt;Paste the URL of the JSON file into the &lt;strong&gt;Import via panel JSON&lt;/strong&gt; field or upload the file directly.&lt;/li&gt;
&lt;li&gt;Assign the dashboard to a folder and Prometheus data source when prompted.&lt;/li&gt;
&lt;/ol&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%2Fwhwn51qahzayp5trh8b4.jpg" 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%2Fwhwn51qahzayp5trh8b4.jpg" alt=" " width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once imported, you can edit the panels to &lt;strong&gt;include additional metrics&lt;/strong&gt;, customize the visualizations, or refine the layout for better insights into your &lt;strong&gt;IRIS &lt;/strong&gt;environment.&lt;/p&gt;





&lt;h3&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;By following this guide, we've successfully set up Prometheus to scrape InterSystems IRIS metrics and visualize them using Grafana. Additionally, you can explore other monitoring tools such as Loki to also monitor logs efficiently and configure alerts using Alertmanager or external services like PagerDuty and Slack. If you have any questions or feedback, feel free to reach out!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>api</category>
      <category>monitoring</category>
      <category>beginners</category>
    </item>
    <item>
      <title>pyprod: Pure Python IRIS Interoperability</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Mon, 20 Jul 2026 15:21:47 +0000</pubDate>
      <link>https://dev.to/intersystems/pyprod-pure-python-iris-interoperability-3kan</link>
      <guid>https://dev.to/intersystems/pyprod-pure-python-iris-interoperability-3kan</guid>
      <description>&lt;p&gt;Intersystems IRIS Productions provide a powerful framework for connecting disparate systems across various protocols and message formats in a reliable, observable, and scalable manner.&amp;nbsp;&lt;strong&gt;intersystems_pyprod&lt;/strong&gt;, short for &lt;em&gt;InterSystems Python Productions&lt;/em&gt;, is a Python library that enables developers to build these interoperability components entirely in Python. Designed for flexibility, it supports a hybrid approach: you can seamlessly mix new Python-based components with existing ObjectScript-based ones, leveraging your established IRIS infrastructure. Once defined, these Python components are managed just like any other; they can be added, configured, and connected using the IRIS Production Configuration page.&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;&lt;strong&gt;A Quick Primer on InterSystems IRIS Productions&lt;/strong&gt;&lt;/h2&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/uxrqudqxu71qyqd568s2.png)
&lt;p&gt;&lt;strong&gt;&lt;u&gt;Key Elements of a Production&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Image from Learning Services training material&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;An IRIS Production generally receives data from external interfaces, processes it through coordinated steps, and routes it to its destination. As messages move through the system, they are automatically persisted, making the entire flow fully traceable through IRIS’s visual trace and logging tools. The architecture relies on certain key elements:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Business Hosts:&lt;/strong&gt;&lt;/b&gt; These are the core building blocks—&lt;b&gt;&lt;strong&gt;Services&lt;/strong&gt;&lt;/b&gt;, &lt;b&gt;&lt;strong&gt;Processes&lt;/strong&gt;&lt;/b&gt;, and &lt;b&gt;&lt;strong&gt;Operations&lt;/strong&gt;&lt;/b&gt;—that pass persistable messages between one another.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Adapters:&lt;/strong&gt;&lt;/b&gt; Inbound and outbound adapters manage the interaction with the external world, handling the specific protocols needed to receive and send data.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Callbacks:&lt;/strong&gt;&lt;/b&gt; The engine uses specific callback methods to pass messages between hosts, either &lt;b&gt;&lt;strong&gt;synchronously or asynchronously&lt;/strong&gt;&lt;/b&gt;. These callbacks follow strict signatures and return a &lt;code&gt;Status&lt;/code&gt; object to ensure execution integrity.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Configuration Helpers:&lt;/strong&gt;&lt;/b&gt; Objects such as &lt;b&gt;&lt;strong&gt;Properties&lt;/strong&gt;&lt;/b&gt; and &lt;b&gt;&lt;strong&gt;Parameters&lt;/strong&gt;&lt;/b&gt; expose settings to the Production Configuration UI, allowing users to easily instantiate, configure, and save the state of these components.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;Workflow using pyprod&lt;/h2&gt;
&lt;p&gt;This is essentially a 3&amp;nbsp;step process.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write your production components&lt;/strong&gt; in a regular Python script. In that script, you import the required base classes from &lt;strong&gt;intersystems_pyprod&lt;/strong&gt;&amp;nbsp;and define your own components by subclassing them, just as you would with any other Python library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load them into InterSystems IRIS&lt;/strong&gt; by running the &lt;code&gt;&lt;strong&gt;intersystems_pyprod&lt;/strong&gt;&lt;/code&gt;&amp;nbsp;(same name as the library) command from the terminal and passing it the path to your Python script. This step links the Python classes with IRIS so that they appear as production components and can be configured and wired together using the standard Production Configuration UI.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create the production&lt;/strong&gt; using the Production Configuration page&amp;nbsp;and start the Production&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;NOTE: If you create all your components with all their Properties hardcoded within the python script, you only need to add them to the production and start the Production.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;You can connect&amp;nbsp;pyprod to your IRIS instance&amp;nbsp;by doing a&amp;nbsp;&lt;a href="https://github.com/intersystems/pyprod/blob/main/docs/installing.md" rel="noopener noreferrer"&gt;&lt;strong&gt;one time setup&lt;/strong&gt;&lt;/a&gt;.&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;&lt;strong&gt;Simple Example&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;In this example, we demonstrate a synchronous message flow where a request originates from a Service, moves through a Process, and is forwarded to an Operation. The resulting response then travels the same path in reverse, passing from the Operation back through the Process to the Service. Additionally, we showcase how to utilize the &lt;code&gt;IRISLog&lt;/code&gt; utility to write custom log entries.&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%2Ft3khatdp059r3gmgs5ee.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%2Ft3khatdp059r3gmgs5ee.png" alt=" " width="742" height="498"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt;Step 1&lt;/h3&gt;
&lt;p&gt;Create your Production components using pyprod in the file HelloWorld.py&lt;/p&gt;
&lt;p&gt;Here are some key parts of the code&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Package Naming:&lt;/strong&gt;&lt;/b&gt; We define &lt;code&gt;iris_&lt;/code&gt;&lt;code&gt;package_name&lt;/code&gt;, which prefixes all classes as they appear on the Production Configuration page&amp;nbsp;(If omitted, the script name is used as the default prefix).&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Persistable Messages:&lt;/strong&gt;&lt;/b&gt; We define &lt;code&gt;MyRequest&lt;/code&gt; and &lt;code&gt;MyResponse&lt;/code&gt;. These are the essential data structures for communication, as only persistable objects can be passed between Services, Processes, and Operations.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;The Inbound Adapter:&lt;/strong&gt;&lt;/b&gt; Our adapter passes a string to the Service using the &lt;code&gt;business_host_process_input&amp;nbsp;&lt;/code&gt;method.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;The Business Service:&lt;/strong&gt;&lt;/b&gt;&amp;nbsp;Implemented with the help of &lt;strong&gt;OnProcessInput&lt;/strong&gt; callback.&lt;ul&gt;
&lt;li&gt;MyService receives data from the adapter and converts it into a &lt;code&gt;MyRequest&lt;/code&gt; message&lt;/li&gt;
&lt;li&gt;We use the &lt;code&gt;ADAPTER&lt;/code&gt;&lt;b&gt;&lt;strong&gt;IRISParameter&lt;/strong&gt;&lt;/b&gt; to link the Inbound Adapter to the Service. Note that this attribute must be named &lt;code&gt;ADAPTER&lt;/code&gt; in all caps to align with IRIS conventions.&lt;/li&gt;
&lt;li&gt;We define a &lt;code&gt;target&lt;/code&gt;&lt;b&gt;&lt;strong&gt;IRISProperty&lt;/strong&gt;&lt;/b&gt;, which allows users to select the destination component directly via the Configuration UI.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Business Process:&lt;/strong&gt; Implemented with the help of &lt;strong&gt;OnRequest&lt;/strong&gt; callback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Business Operation&lt;/strong&gt;: Implemented with the help of &lt;strong&gt;OnMessage&lt;/strong&gt; callback. (You can also define a MessageMap)&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Logic &amp;amp; Callbacks:&lt;/strong&gt;&lt;/b&gt; Finally, the hosts implement their core logic within standard callbacks like &lt;code&gt;OnProcessInput&lt;/code&gt; and &lt;code&gt;OnRequest&lt;/code&gt;, routing messages using the &lt;code&gt;SendRequestSync&lt;/code&gt; method.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can read more about each of these parts on the pyprod &lt;a href="https://github.com/intersystems/pyprod/blob/main/docs/apireference.md#pyprod--api-reference" rel="noopener noreferrer"&gt;&lt;strong&gt;API Reference page&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;and also using the &lt;a href="https://github.com/intersystems/pyprod/blob/main/docs/quickstart.md#quick-start-guide" rel="noopener noreferrer"&gt;&lt;strong&gt;Quick Start Guide&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;import&lt;/span&gt; time

&lt;p&gt;&lt;span&gt;from&lt;/span&gt; intersystems_pyprod &lt;span&gt;import&lt;/span&gt; (&lt;br&gt;
    InboundAdapter,BusinessService, BusinessProcess, &lt;br&gt;
    BusinessOperation, OutboundAdapter, JsonSerialize, &lt;br&gt;
    IRISProperty, IRISParameter, IRISLog, Status)&lt;/p&gt;

&lt;p&gt;iris_package_name = &lt;span&gt;"helloworld"&lt;/span&gt;&lt;br&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyRequest&lt;/span&gt;&lt;span&gt;(JsonSerialize)&lt;/span&gt;:&lt;br&gt;
    content: str&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyResponse&lt;/span&gt;&lt;span&gt;(JsonSerialize)&lt;/span&gt;:&lt;br&gt;
    content: str&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyInAdapter&lt;/span&gt;&lt;span&gt;(InboundAdapter)&lt;/span&gt;:&lt;br&gt;
    &lt;span&gt;def&lt;/span&gt; &lt;span&gt;OnTask&lt;/span&gt;&lt;span&gt;(self)&lt;/span&gt;:&lt;br&gt;
        time.sleep(&lt;span&gt;0.5&lt;/span&gt;)&lt;br&gt;
        self.business_host_process_input(&lt;span&gt;"request message"&lt;/span&gt;)&lt;br&gt;
        &lt;span&gt;return&lt;/span&gt; Status.OK()&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyService&lt;/span&gt;&lt;span&gt;(BusinessService)&lt;/span&gt;:&lt;br&gt;
    ADAPTER = IRISParameter(&lt;span&gt;"helloworld.MyInAdapter"&lt;/span&gt;)&lt;br&gt;
    target = IRISProperty(settings=&lt;span&gt;"Target"&lt;/span&gt;)&lt;br&gt;
    &lt;span&gt;def&lt;/span&gt; &lt;span&gt;OnProcessInput&lt;/span&gt;&lt;span&gt;(self, input)&lt;/span&gt;:&lt;br&gt;
        persistent_message = MyRequest(input)&lt;br&gt;
        status, response = self.SendRequestSync(self.target, persistent_message)&lt;br&gt;
        IRISLog.Info(response.content)&lt;br&gt;
        &lt;span&gt;return&lt;/span&gt; status&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyProcess&lt;/span&gt;&lt;span&gt;(BusinessProcess)&lt;/span&gt;:&lt;br&gt;
    target = IRISProperty(settings=&lt;span&gt;"Target"&lt;/span&gt;)&lt;br&gt;
    &lt;span&gt;def&lt;/span&gt; &lt;span&gt;on_request&lt;/span&gt;&lt;span&gt;(self, input)&lt;/span&gt;:&lt;br&gt;
        status, response = self.SendRequestSync(self.target,input)&lt;br&gt;
        &lt;span&gt;return&lt;/span&gt; status, response&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyOperation&lt;/span&gt;&lt;span&gt;(BusinessOperation)&lt;/span&gt;:&lt;br&gt;
    ADAPTER = IRISParameter(&lt;span&gt;"helloworld.MyOutAdapter"&lt;/span&gt;)&lt;br&gt;
    &lt;span&gt;def&lt;/span&gt; &lt;span&gt;OnMessage&lt;/span&gt;&lt;span&gt;(self, input)&lt;/span&gt;:&lt;br&gt;
        status = self.ADAPTER.custom_method(input)&lt;br&gt;
        response = MyResponse(&lt;span&gt;"response message"&lt;/span&gt;)&lt;br&gt;
        &lt;span&gt;return&lt;/span&gt; status, response&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyOutAdapter&lt;/span&gt;&lt;span&gt;(OutboundAdapter)&lt;/span&gt;:&lt;br&gt;
    &lt;span&gt;def&lt;/span&gt; &lt;span&gt;custom_method&lt;/span&gt;&lt;span&gt;(self, input)&lt;/span&gt;:&lt;br&gt;
        IRISLog.Info(input.content)&lt;br&gt;
        &lt;span&gt;return&lt;/span&gt; Status.OK()&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2&lt;/h3&gt;
&lt;p&gt;Once your code is ready, load the components to IRIS.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ intersystems_pyprod /full/path/to/HelloWorld.py

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Loading MyRequest to IRIS...
...
Load finished successfully.

Loading MyResponse to IRIS...
...
Load finished successfully.
...
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;h3&amp;gt;Step 3&amp;lt;/h3&amp;gt;&amp;lt;p&amp;gt;Add each host to the Production using the Production Configuration page.&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;The image below shows&amp;nbsp;&amp;lt;code data-index-in-node="42" data-path-to-node="3"&amp;gt;MyService&amp;lt;/code&amp;gt;&amp;nbsp;and its &amp;lt;code data-index-in-node="93" data-path-to-node="3"&amp;gt;target&amp;lt;/code&amp;gt; property&amp;nbsp;being configured through the UI. Follow the same process to add &amp;lt;code data-index-in-node="176" data-path-to-node="3"&amp;gt;MyProcess&amp;lt;/code&amp;gt; and &amp;lt;code data-index-in-node="190" data-path-to-node="3"&amp;gt;MyOperation&amp;lt;/code&amp;gt;. Once the setup is complete, simply start the production to see your messages in motion.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&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%2F3k3972p7c0w53tnajuez.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%2F3k3972p7c0w53tnajuez.png" alt=" " width="552" height="604"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;By combining the flexibility of the Python ecosystem with the industrial-grade reliability of InterSystems IRIS, &lt;b&gt;&lt;strong&gt;pyprod&lt;/strong&gt;&lt;/b&gt; offers a modern path for building interoperability solutions. Whether you are developing entirely new "Pure Python" productions or enhancing existing ObjectScript infrastructures with specialized Python libraries, &lt;b&gt;&lt;strong&gt;pyprod&lt;/strong&gt;&lt;/b&gt; ensures your components remain fully integrated, observable, and easy to configure.&amp;nbsp;We look forward to seeing what you build!&lt;/p&gt;

&lt;h2&gt;Quick Links&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/intersystems/pyprod" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pypi.org/project/intersystems-pyprod/" rel="noopener noreferrer"&gt;PyPi Package&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;b&gt;&lt;strong&gt;Support the Project:&lt;/strong&gt;&lt;/b&gt; If you find this library useful, please&amp;nbsp;consider &lt;b&gt;&lt;strong&gt;giving us a ⭐ on GitHub&amp;nbsp;&lt;/strong&gt;&lt;/b&gt;and suggesting enhancements. It helps the project grow and makes it easier for other developers in the InterSystems community to discover it!&lt;/blockquote&gt;


&lt;/code&gt;&lt;/pre&gt;

</description>
      <category>programming</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Getting started with OAuth in your Web Apps</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 30 Jun 2026 16:45:19 +0000</pubDate>
      <link>https://dev.to/intersystems/getting-started-with-oauth-in-your-web-apps-2g76</link>
      <guid>https://dev.to/intersystems/getting-started-with-oauth-in-your-web-apps-2g76</guid>
      <description>&lt;p&gt;This article is intended as a beginner level article for people that want to learn how to use OAuth2 in their web applications natively.&lt;/p&gt;
&lt;p&gt;There is an accompanying video/demo that may be helpful here:&amp;nbsp;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/4mfWQwcKcMI"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;and you can reproduce this locally with the Open Exchange application attached.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;&lt;u&gt;OAuth2 as a native authentication type for web applications&lt;/u&gt;&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;OAuth (&lt;strong&gt;O&lt;/strong&gt;pen &lt;strong&gt;Auth&lt;/strong&gt;orization) 2.0 is a standard way to let one application call another application’s API &lt;strong&gt;without&lt;/strong&gt; sharing a username and password. Instead of sending credentials on every request, the client sends an &lt;strong&gt;access token&lt;/strong&gt; (typically in an &lt;code&gt;Authorization: Bearer ...&lt;/code&gt; header).&lt;/p&gt;
&lt;p&gt;OAuth2 focuses on &lt;em&gt;authorization&lt;/em&gt; (what the client is allowed to do). If you also need user login and identity claims, OAuth2 is commonly paired with OpenID Connect (OIDC) — but in this article we’ll stay focused on OAuth2 access tokens and scopes.&lt;/p&gt;
&lt;p&gt;If you want a quick refresher, this short video is a good overview: &lt;a href="https://learning.intersystems.com/course/view.php?name=OAuth%202.0:%20An%20Overview" rel="noopener noreferrer"&gt;OAuth 2.0 An Overview&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;The problem OAuth2 solves (with a simple IRIS example)&lt;/h3&gt;
&lt;p&gt;Assume IRIS hosts a small REST API for a bank account &lt;code&gt;ACCT-1&lt;/code&gt; under &lt;code&gt;/bank&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GET&lt;/strong&gt;&lt;br&gt;&lt;code&gt;/bank/checkbalance&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &lt;span class="mention"&gt;"dollars"&lt;/span&gt;: &lt;span class="mention"&gt;5&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;POST&lt;/strong&gt;&lt;br&gt;&lt;code&gt;/bank/transfer&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &lt;span class="mention"&gt;"toAccount"&lt;/span&gt;: &lt;span class="mention"&gt;"ACCT-2"&lt;/span&gt;,
  &lt;span class="mention"&gt;"dollars"&lt;/span&gt;: &lt;span class="mention"&gt;2&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now suppose you want to allow a third-party app to monitor your balance. It should be allowed to call &lt;code&gt;/checkbalance&lt;/code&gt;, but it should &lt;strong&gt;not&lt;/strong&gt; be allowed to call &lt;code&gt;/transfer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is where OAuth2 fits well: instead of giving the third-party app your IRIS username/password, you grant it limited access via a token. That token can be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scoped&lt;/strong&gt; (e.g., “read balance” but not “transfer funds”)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-limited&lt;/strong&gt; (tokens expire)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocable&lt;/strong&gt; (you can withdraw access later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What’s new in IRIS&lt;/h3&gt;
&lt;p&gt;Starting in &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCRN_new20252#GCRN_new20252_speed" rel="noopener noreferrer"&gt;IRIS 2025.2&lt;/a&gt;, OAuth2 can be selected as a native authentication method for Web Applications — so enabling an OAuth2-protected web app is no longer a “DIY” exercise.&lt;/p&gt;
&lt;p&gt;Concretely, IRIS can validate an incoming access token for a CSP/Web Application request and then establish a user context (username + roles) based on that token, just like other authentication types do.&lt;/p&gt;
&lt;p&gt;(For reference on the older, more manual approach, see &lt;a class="mentioned-user" href="https://dev.to/daniel"&gt;@daniel&lt;/a&gt;.Kutac’s excellent &lt;a href="https://community.intersystems.com/post/intersystems-iris-open-authorization-framework-oauth-20-implementation-part-1" rel="noopener noreferrer"&gt;series of articles&lt;/a&gt;.)&lt;/p&gt;
&lt;h3&gt;The Characters&lt;/h3&gt;
&lt;p&gt;OAuth has a few “characters”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource Owner&lt;/strong&gt; (the user/owner of the bank account)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt; (the third-party app; in this demo we use Postman as the client)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization Server&lt;/strong&gt; (Keycloak; authenticates the user &amp;amp; authorizes the request, deciding what scopes the client can receive, and issues the token)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Server&lt;/strong&gt; (IRIS; hosts &lt;code&gt;/myBankInfo&lt;/code&gt;, validates the token, and enforces what the token is allowed to do). The third-party app never sees your IRIS password — it presents a token, and IRIS makes the allow/deny decision.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 0: Prerequisites (avoid issuer / hostname issues)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This demo uses HTTP to keep setup simple. In production you should use HTTPS (and real certificates), otherwise tokens and sessions can be intercepted.&lt;/p&gt;
&lt;p&gt;This Open Exchange demo runs multiple Docker containers. One important rule to remember is:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;
&lt;code&gt;&lt;strong&gt;localhost&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt; on your host is not the same as &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;localhost&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt; inside a container.&lt;/strong&gt;
&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;OAuth token validation checks the token’s &lt;strong&gt;issuer&lt;/strong&gt; claim (&lt;code&gt;iss&lt;/code&gt;). If Keycloak issues a token with an issuer like &lt;code&gt;http://localhost:8080/...&lt;/code&gt; but IRIS discovers/validates it using &lt;code&gt;http://keycloak:8080/...&lt;/code&gt;, IRIS will reject the token because those issuers do not match.&lt;/p&gt;
&lt;p&gt;To keep the issuer stable, this demo uses the hostname &lt;code&gt;&lt;strong&gt;keycloak&lt;/strong&gt;&lt;/code&gt; consistently from both the host and the containers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On Windows&lt;/strong&gt;, edit: &lt;em&gt;C:\Windows\System32\drivers\etc\hosts&lt;/em&gt; and add:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;127.0.0.1 keycloak&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;On Linux/Mac&lt;/strong&gt;, edit &lt;em&gt;/etc/hosts&lt;/em&gt; and add the same line (you’ll typically need sudo).&lt;/p&gt;
&lt;p&gt;From this point on, use &lt;strong&gt;http://keycloak:8080&lt;/strong&gt; (not &lt;code&gt;http://localhost:8080&lt;/code&gt;) when configuring Postman and IRIS.&lt;/p&gt;
&lt;h3&gt;Step 1: Configure the Authorization Server (Keycloak)&lt;/h3&gt;
&lt;p&gt;For the demo, the Authorization Server is Keycloak and it is already prepared for this use case (realm, clients, users, scopes). No work is needed here.&lt;/p&gt;
&lt;p&gt;You can access the Keycloak admin console at &lt;a href="http://keycloak:8080/keycloak/admin/master/console/" rel="noopener noreferrer"&gt;http://keycloak:8080/keycloak/admin/master/console/&lt;/a&gt; (username/password &lt;code&gt;admin/admin&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Explaining Keycloak itself is not in the scope for this article, but if you would like to read more you can find the docs &lt;a href="https://www.keycloak.org/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Step 2: Tell IRIS who the Authorization Server is&lt;/h3&gt;
&lt;p&gt;In the Management Portal, go to:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;System Administration &amp;gt; Security &amp;gt; OAuth 2.0 &amp;gt; Client&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Click &lt;em&gt;Create Server Description&lt;/em&gt;, set the Issuer URL (in the demo: &lt;code&gt;http://keycloak:8080/keycloak/realms/bank&lt;/code&gt;), then click &lt;em&gt;Discover&lt;/em&gt; and &lt;em&gt;Save&lt;/em&gt;. IRIS will pull the endpoints and metadata it needs from the server (authorization endpoint, token endpoint, JWKS URI, etc.).&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%2F1zv1wzzmxju2gdzptatd.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%2F1zv1wzzmxju2gdzptatd.png" alt=" " width="799" height="284"&gt;&lt;/a&gt;&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%2F7bxmy7spsjgv5cjvvzbi.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%2F7bxmy7spsjgv5cjvvzbi.png" alt=" " width="800" height="849"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt;Step 3: Configure IRIS as the Resource Server&lt;/h3&gt;
&lt;p&gt;Next, create a Resource Server entry so IRIS can validate tokens and enforce permissions:&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%2Fixmudlbnica1sdog3loe.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%2Fixmudlbnica1sdog3loe.png" alt=" " width="633" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;em&gt;Create Resource Server&lt;/em&gt;:&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%2F2oo938k51v78c0msumyf.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%2F2oo938k51v78c0msumyf.png" alt=" " width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fill in the details of your resource server, for example:&lt;/p&gt;
&lt;p&gt;Name: IRIS Bank Resource Server&lt;/p&gt;
&lt;p&gt;Server Definition:&amp;nbsp;&lt;code&gt;http://keycloak:8080/keycloak/realms/bank&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Audiences: bank-demo, bank-monitor&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is “Audience”?&lt;/strong&gt; The token’s audience (&lt;code&gt;aud&lt;/code&gt;) is the “intended recipient” of the token. By configuring audiences here, you are telling IRIS to accept only tokens that were issued for this API (i.e., tokens whose &lt;code&gt;aud&lt;/code&gt; matches one of these values).&lt;/p&gt;
&lt;p&gt;Click save.&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%2Fdqfdroqs1b1k7is1p0d7.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%2Fdqfdroqs1b1k7is1p0d7.png" alt=" " width="710" height="746"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will set the Authenticator class in the next step. Note that this is not strictly necessary; you could use the &lt;a href="https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic.cls?LIBRARY=%25SYS&amp;amp;CLASSNAME=%25OAuth2.ResourceServer.SimpleAuthenticator" rel="noopener noreferrer"&gt;%OAuth2.ResourceServer.SimpleAuthenticator&lt;/a&gt; in your own implementations and just fill in what token property should be attributed to the role and user. However, for the sake of completeness we will create a simple custom authenticator class.&lt;/p&gt;
&lt;h3&gt;Step 4: Create your Authenticator Class&lt;/h3&gt;
&lt;p&gt;What should be authenticated? We will create a simple class &lt;code&gt;Bank.Authenticator&lt;/code&gt; that maps token claims/scopes into an IRIS username and IRIS roles.&lt;/p&gt;
&lt;p&gt;This is the key step that lets IRIS enforce “read-only” vs “transfer” behavior:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The token’s &lt;strong&gt;scopes&lt;/strong&gt; become &lt;strong&gt;IRIS roles&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Your &lt;strong&gt;web application&lt;/strong&gt; (and/or your REST endpoints) can require those roles.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, this is what makes &lt;code&gt;/checkbalance&lt;/code&gt;&amp;nbsp; succeed for a “monitor” token while &lt;code&gt;/transfer&lt;/code&gt; returns &lt;strong&gt;403 Forbidden&lt;/strong&gt; unless the token includes the transfer scope.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;Class&lt;/span&gt; Bank.Authenticator &lt;span class="mention"&gt;Extends&lt;/span&gt; &lt;span class="mention"&gt;%OAuth&lt;/span&gt;2.ResourceServer.Authenticator
{

&lt;span class="mention"&gt;ClassMethod&lt;/span&gt; HasScope(scopeStr &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%String&lt;/span&gt;, scope &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%String&lt;/span&gt;) &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%Boolean&lt;/span&gt;
{
    &lt;span class="mention"&gt;Quit&lt;/span&gt; ((&lt;span class="mention"&gt;" "&lt;/span&gt;_scopeStr_&lt;span class="mention"&gt;" "&lt;/span&gt;) [ (&lt;span class="mention"&gt;" "&lt;/span&gt;_scope_&lt;span class="mention"&gt;" "&lt;/span&gt;))
}

Method Authenticate(claims &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%DynamicObject&lt;/span&gt;, oidc &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%Boolean&lt;/span&gt;, Output properties &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%String&lt;/span&gt;) &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%Status&lt;/span&gt;
{
    &lt;span class="mention"&gt;// Map token -&amp;gt; IRIS username&lt;/span&gt;
    &lt;span class="mention"&gt;Set&lt;/span&gt; properties(&lt;span class="mention"&gt;"Username"&lt;/span&gt;) = claims.&lt;span class="mention"&gt;"preferred_username"&lt;/span&gt;
    &lt;span class="mention"&gt;// Map scopes -&amp;gt; IRIS roles&lt;/span&gt;
    &lt;span class="mention"&gt;Set&lt;/span&gt; scopeStr = claims.scope
    &lt;span class="mention"&gt;Set&lt;/span&gt; roles = &lt;span class="mention"&gt;""&lt;/span&gt;
    &lt;span class="mention"&gt;If&lt;/span&gt; &lt;span class="mention"&gt;..HasScope&lt;/span&gt;(scopeStr,&lt;span class="mention"&gt;"bank.balance.read"&lt;/span&gt;) {
        &lt;span class="mention"&gt;Set&lt;/span&gt; roles = roles_&lt;span class="mention"&gt;",BankBalanceRead,%DB_USER"&lt;/span&gt;
    }
    &lt;span class="mention"&gt;If&lt;/span&gt; &lt;span class="mention"&gt;..HasScope&lt;/span&gt;(scopeStr,&lt;span class="mention"&gt;"bank.transfer.write"&lt;/span&gt;) {
        &lt;span class="mention"&gt;Set&lt;/span&gt; roles = roles_&lt;span class="mention"&gt;",BankTransferWrite,%DB_USER"&lt;/span&gt;
    }

    &lt;span class="mention"&gt;If&lt;/span&gt; &lt;span class="mention"&gt;$Extract&lt;/span&gt;(roles,&lt;span class="mention"&gt;1&lt;/span&gt;)=&lt;span class="mention"&gt;","&lt;/span&gt; &lt;span class="mention"&gt;Set&lt;/span&gt; roles=&lt;span class="mention"&gt;$Extract&lt;/span&gt;(roles,&lt;span class="mention"&gt;2&lt;/span&gt;,*)
    
    &lt;span class="mention"&gt;Set&lt;/span&gt; properties(&lt;span class="mention"&gt;"Roles"&lt;/span&gt;) = roles
    &lt;span class="mention"&gt;Quit&lt;/span&gt; &lt;span class="mention"&gt;$$$OK&lt;/span&gt;
}

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you compile the class you will be able to set your authenticator class in your resource server:&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%2Fm3w2ivquudx3a3tbhjnd.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%2Fm3w2ivquudx3a3tbhjnd.png" alt=" " width="596" height="84"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Save your resource server.&lt;/p&gt;
&lt;h3&gt;Step 5: Enable OAuth2 on the Web Application&lt;/h3&gt;
&lt;p&gt;Before enabling OAuth2 for a web app, you must enable it at the System level:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;System Administration &amp;gt; Security &amp;gt; System Security &amp;gt; Authentication/Web Session Options&lt;/em&gt;&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%2Frtfo4ojcvhxceg6dzhto.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%2Frtfo4ojcvhxceg6dzhto.png" alt=" " width="800" height="592"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, on your Web Application definition, select &lt;strong&gt;OAuth2&lt;/strong&gt; as an allowed authentication method. The dispatch class will check that the client has the necessary roles.&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%2Fi7w9dmwujexq08s5ahig.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%2Fi7w9dmwujexq08s5ahig.png" alt=" " width="800" height="487"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt;Step 6: Test it out&lt;/h3&gt;
&lt;p&gt;At this point, requests to your application can be authorized based on the presented token — so you can allow read-only access to &lt;code&gt;/checkbalance&lt;/code&gt;&amp;nbsp;while denying access to &lt;code&gt;/transfer&lt;/code&gt; using the OAuth2 framework.&lt;/p&gt;
&lt;p&gt;Load the Postman collection and environment. There are two demo users/passwords to have in mind: &lt;code&gt;user1/123&lt;/code&gt; and &lt;code&gt;user2/123&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;User 1 has account &lt;code&gt;ACCT-1&lt;/code&gt;, User 2 has account &lt;code&gt;ACCT-2&lt;/code&gt;.&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%2Fuq8lfej5wq4onxplbsty.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%2Fuq8lfej5wq4onxplbsty.png" alt=" " width="542" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Postman, on Authorization click &lt;em&gt;Get New Access Token&lt;/em&gt;:&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%2Fnw20i0mxxivw0yp78d9p.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%2Fnw20i0mxxivw0yp78d9p.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This brings up the login screen for our Authorization Server:&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%2Fggcyxhb9ciy4qptk5jwq.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%2Fggcyxhb9ciy4qptk5jwq.png" alt=" " width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Log in with &lt;code&gt;user1/123&lt;/code&gt;. Click proceed and then click &lt;em&gt;Use Token&lt;/em&gt;.&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%2Fgpnxsyzjxw1z08r1dsxq.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%2Fgpnxsyzjxw1z08r1dsxq.png" alt=" " width="710" height="138"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Send your GET to &lt;code&gt;/checkbalance&lt;/code&gt;&amp;nbsp;and you should see it return 5 dollars:&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%2Fovkq7p71brjbwbqlt1sp.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%2Fovkq7p71brjbwbqlt1sp.png" alt=" " width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clear cookies and try logging in with user 2 and you should see them have 0 dollars in their balance.&lt;/p&gt;
&lt;p&gt;Now get a token for user 1 and try to transfer user 2 a couple dollars. It should fail with &lt;strong&gt;403 Forbidden&lt;/strong&gt;&amp;nbsp;as this “app” does not have the required scopes (it is only monitoring the bank account and should not be able to transfer money).&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%2Fagwxt6ligch3sdktggt4.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%2Fagwxt6ligch3sdktggt4.png" alt=" " width="799" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try again with requests 3 and 4 which simulate a client with full access and you should be able to both check your balance and transfer funds.&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%2Fupjdudkgdsbbvrkbfsjw.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%2Fupjdudkgdsbbvrkbfsjw.png" alt=" " width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The new OAuth2 native authentication type ensures it is intuitive to keep your web applications safe, and after all, that's what the I in IRIS is all about.&lt;/p&gt;

</description>
      <category>api</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Explainability in ML Models</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 30 Jun 2026 16:32:00 +0000</pubDate>
      <link>https://dev.to/intersystems/explainability-in-ml-models-4300</link>
      <guid>https://dev.to/intersystems/explainability-in-ml-models-4300</guid>
      <description>&lt;p&gt;This article introduces SHAP explainability methods as an approach to understand the reasons behind predictions in machine learning black-box models. It also includes a simple Jupyter notebook that you can use and modify to gain hands-on experience with these concepts:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.kaggle.com/code/jorgeivnjh/explainability-in-ml-models" rel="noopener noreferrer"&gt;https://www.kaggle.com/code/jorgeivnjh/explainability-in-ml-models&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/JorgeIvanJH/Explainability-in-ML-models" rel="noopener noreferrer"&gt;https://github.com/JorgeIvanJH/Explainability-in-ML-models&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will leverage these concepts for a future implementation in our Continuous Training Pipeline: &lt;a href="https://community.intersystems.com/post/complementing-iris-mlflow-continuous-training-ct-pipeline" rel="noopener noreferrer"&gt;https://community.intersystems.com/post/complementing-iris-mlflow-continuous-training-ct-pipeline&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;In this notebook, we provide intuition about explainability for black-box models. Black-box models are those that are too complex for a human to directly understand, such as neural networks and ensemble methods like gradient boosting (e.g. XGBoost, LightGBM, CatBoost).&lt;/p&gt;

&lt;p&gt;Before starting, it is worth clarifying the difference between interpretable models and explainable models:&lt;/p&gt;

&lt;p&gt;Interpretable models are those where we can directly understand how changes in the inputs affect the output, just by looking at the model itself.&lt;br&gt;
This is the case for linear regression, where each variable is associated with a coefficient that indicates how it influences the prediction. It is also true for a single decision tree, where, by following the branches, we can understand exactly how a prediction is made.&lt;/p&gt;

&lt;p&gt;In contrast, models such as random forests and gradient boosting (e.g. XGBoost, LightGBM), which combine many trees, or neural networks with thousands or millions of parameters, are too complex for this type of direct interpretation. In these cases, we rely on explainability methods to understand how the model is using the input features to produce its predictions.&lt;/p&gt;

&lt;p&gt;To provide explainability for such models, we typically take a trained model and analyse how changes in the input features affect its output. There are many approaches to do this (e.g. partial dependence plots, ICE plots, LIME), but one of the most widely used and mathematically grounded methods is SHAP.&lt;/p&gt;

&lt;p&gt;SHAP (SHapley Additive exPlanations) is based on game theory and computes Shapley values, which quantify how much each feature contributes to a model’s prediction. These contributions can be analysed both globally (across the dataset) and locally (for individual predictions).&lt;/p&gt;

&lt;p&gt;In this notebook, we use the SHAP python library to explore these ideas. We start with a simple, interpretable model (linear regression), and then move to a more complex model (LightGBM). Along the way, we introduce some of the most commonly used plots to explain model behaviour.&lt;/p&gt;

&lt;p&gt;Note: To run this notebook in Kaggle you must have logged in with your account and have access to internet (Settings - Turn on internet)&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;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="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;sklearn&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shap&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;plotly.express&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lightgbm&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt; &lt;span class="c1"&gt;# for a quick lightgbm hyperparameter tuning
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filenames&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/kaggle/input&lt;/span&gt;&lt;span class="sh"&gt;'&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;filename&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;filenames&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this exercise, we leverage the California Housing dataset as ground truth. Which takes the following variables associated with the price of the house:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MedInc (float): Median income in block&lt;/li&gt;
&lt;li&gt;HouseAge (float): Median house age in block&lt;/li&gt;
&lt;li&gt;AveRooms (float): Average rooms in dwelling&lt;/li&gt;
&lt;li&gt;AveBedrms (float): Average bedrooms in dwelling&lt;/li&gt;
&lt;li&gt;Population (float): Block population&lt;/li&gt;
&lt;li&gt;AveOccup (float): Average house occupancy&lt;/li&gt;
&lt;li&gt;Latitude (float): House block latitude&lt;/li&gt;
&lt;li&gt;Longitude (float): House block longitude&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To predict the variable of interest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MedHouseVal (float): Median House Value, expressed in units of $100,000 (e.g., a value of 4.526 represents $452,600)
&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="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datasets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;california&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_points&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# The dataset
&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Linear Regression (Interpretable model)
&lt;/h1&gt;

&lt;p&gt;Linear regression is a simple yet powerful model that is particularly easy to interpret. This is because, after the model is fit, each variable is associated with a parameter (coefficient) that it is multiplied by, transforming its value into the units of the variable of interest. The sum of all these transformed variables, plus an offset (intercept), is what produces the final prediction.&lt;/p&gt;

&lt;p&gt;For example, by analysing the parameters of an already fit house price prediction model, we can get an idea of how each variable influences the price of a house in our dataset:&lt;/p&gt;

&lt;p&gt;HousePrice = MedInc * (0.4) + AveBedrms * (5000) + Latitude * (-0.5) + 50000&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The offset suggests that the baseline value of a house, when all variables are zero, is 50,000.&lt;/li&gt;
&lt;li&gt;Median income in the block (MedInc) increases the price of the house at a rate of 0.4 (each additional dollar in income adds 0.4 dollars to the predicted house price), and each additional bedroom adds 5,000 dollars.&lt;/li&gt;
&lt;li&gt;Latitude has a negative coefficient, suggesting that as we move north (latitude increases), house prices decrease. Since latitude increases northward, the negative coefficient implies a downward effect on price.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This initial interpretation is useful; however, there is an important limitation: the scale of the parameters can be misleading without context. If variables are not normalised, those with large numerical scales will tend to have smaller coefficients, and vice versa.&lt;/p&gt;

&lt;p&gt;For example, if median house age ("HouseAge") were measured in seconds instead of years, its coefficient would be much smaller, simply because the input values are much larger. This could give the misleading impression that HouseAge is less important than other variables, when in reality the difference is only due to the units of measurement. In contrast, a variable like AveBedrms might have a larger coefficient simply because it operates on a smaller numerical scale.&lt;/p&gt;

&lt;p&gt;Now to the actual model on our dataset:&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;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linear_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LinearRegression&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model coefficients:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coef_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Intercept = &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intercept_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Model performance metrics:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;r2_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_valid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;mae&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean_absolute_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_valid&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r2 score: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;r2&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mae: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;mae&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model coefficients:

MedInc = 0.41174
HouseAge = 0.00932
AveRooms = -0.109
AveBedrms = 0.63208
Population = 4e-05
AveOccup = -0.25315
Latitude = -0.46534
Longitude = -0.46173
Intercept =  -37.84145

Model performance metrics:

r2 score:  0.6822806366957364
mae:  0.5473856962442348
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The analysis we did works well, but it provides a static, global understanding of how each variable affects the output across all samples. It fails to capture interactions between variables and is mainly limited to linear models. If we were using more complex models (e.g. neural networks or tree-based models), this type of interpretation would not be sufficient to understand the relationships being learned.&lt;/p&gt;

&lt;p&gt;This is where SHAP comes in. SHAP (Shapley values) is a method based on game theory that allows us to understand the marginal contribution of each feature to a model’s prediction. We will omit the theory behind it, but intuitively, it tells us how each feature steers a prediction away from the model’s average prediction. To make this easier to understand, we can compare it to linear regression. In linear regression, the intercept acts as a baseline, and each feature multiplied by its coefficient shifts the prediction away from that baseline. In SHAP, the baseline is the average prediction of the model across the dataset (the expected value), and the Shapley value of each feature represents how much that feature contributes to moving from this baseline to the final prediction for a given sample.&lt;/p&gt;

&lt;p&gt;Unlike linear regression coefficients, which provide a single global interpretation, SHAP allows us to compute per-sample explanations, showing how each feature contributes to an individual prediction, not just on average across the dataset.&lt;/p&gt;

&lt;p&gt;Below, we take a subsample of the data to use as a reference dataset for comparisons (we could use the full dataset, but that would be computationally expensive). We then create an explainer object for the linear regression model we trained, compute the SHAP values (which represent the contribution of each feature to each prediction), and select a specific sample (sample #20) to analyse the relationships captured by the model.&lt;/p&gt;

&lt;p&gt;Note: To understand the underlying SHAP algorithm, refer to: &lt;a href="https://christophm.github.io/interpretable-ml-book/shapley.html" rel="noopener noreferrer"&gt;https://christophm.github.io/interpretable-ml-book/shapley.html&lt;/a&gt;&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;X100&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Subsample to use as background dataset (SHAP needs one for its internal algorithm)
&lt;/span&gt;&lt;span class="n"&gt;explainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Explainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;shap_values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;explainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sample_ind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ExactExplainer explainer: 901it [00:12, 30.31it/s]                          
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having SHAP values computed allows us to draw different plots to better understand the reasons behind a model’s predictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependence plot (+ ICE lines)
&lt;/h2&gt;

&lt;p&gt;A dependence plot helps us understand how the model output changes as a feature varies, as well as how frequently different values of that feature occur in the data. More specifically, it shows how the model’s prediction evolves across the range of a feature, while also giving a sense of how common those values are. This helps us see not only the effect of a feature, but also how relevant that effect is in practice. As a result, values that have a strong effect but occur rarely may end up being less important overall than values that have a weaker effect but occur frequently.&lt;/p&gt;

&lt;p&gt;In the plot below, we overlay both the dependence plot and the Individual Conditional Expectation (ICE) lines, displaying one line per instance that shows how the instance’s prediction changes when a feature changes. We show the behaviour for the variable "Latitude":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The average value of the feature Latitude (grey vertical dashed line), at around 36&lt;/li&gt;
&lt;li&gt;The average model prediction for the price of a house (grey horizontal dashed line), at nearly 2 ($200.000).&lt;/li&gt;
&lt;li&gt;The bold blue line represents the average model prediction as we vary Latitude across its range (this is the partial dependence, i.e. the global trend)&lt;/li&gt;
&lt;li&gt;The lighter blue lines represent the model predictions for individual samples as we vary Latitude (ICE curves). Each line corresponds to one sample, where we change only the Latitude and keep the rest of the features fixed&lt;/li&gt;
&lt;li&gt;The red vertical segment marks the selected sample (sample_ind). It shows how that specific sample’s prediction shifts relative to the baseline (expected value), highlighting the contribution of Latitude for that instance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the blue lines are linear because the underlying model is a linear regression. We can clearly see a negative relationship: as Latitude increases (moving north), the predicted house price decreases across all samples.&lt;/p&gt;

&lt;p&gt;The spread of ICE lines does not indicate how important a feature is, but rather how consistent its effect is across samples. If the lines are tightly grouped, the feature has a similar effect across the dataset. If they are widely spread, the feature interacts with other variables, and its effect depends on the specific sample.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partial_dependence_plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;X100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ice&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="c1"&gt;# Change to false to see only general trend
&lt;/span&gt;    &lt;span class="n"&gt;model_expected_value&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;feature_expected_value&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;shap_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sample_ind&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sample_ind&lt;/span&gt; &lt;span class="o"&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fqk8tz6w14jmds2ryaqsd.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%2Fqk8tz6w14jmds2ryaqsd.png" alt=" " width="624" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &amp;nbsp;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Scatter Plot
&lt;/h2&gt;

&lt;p&gt;Another way to visualise how a variable influences the model output is through a scatter plot. In this plot, we place the feature values on the x-axis and their corresponding SHAP values on the y-axis, showing how changes in the feature affect the prediction.&lt;/p&gt;

&lt;p&gt;By passing shap_values to the "color" argument, SHAP automatically selects the feature that is most strongly correlated (or interacting) with the SHAP values of the selected feature, and uses it to colour the points.&lt;/p&gt;

&lt;p&gt;In our example, we analyse Latitude, and SHAP identifies Longitude as the feature most related to it, which is then shown through the colour scale. (Please note that this is consistent with the wide spread on the lines in the ICE plot above associated with interaction with another variable)&lt;/p&gt;

&lt;p&gt;In this plot, we observe that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Points with lower latitude (further south) tend to have higher longitude values (red), meaning they are also located more to the east&lt;/li&gt;
&lt;li&gt;Points with higher latitude (further north) tend to have lower longitude values (blue), meaning they are also located more to the west&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern is consistent with the density of houses around the two main population centres in California: San Francisco (northwest) and Los Angeles (southeast).&lt;/p&gt;

&lt;p&gt;The SHAP values follow a clear negative linear trend (as expected from a linear model), showing that as latitude increases, its contribution to the prediction decreases.&lt;/p&gt;

&lt;p&gt;This implies that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Houses in the southeast (low latitude, high longitude) tend to have a positive contribution to the predicted price&lt;/li&gt;
&lt;li&gt;Houses in the northwest (high latitude, low longitude) tend to have a negative contribution to the predicted price&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, in this dataset, the model associates southeastern locations with higher predicted prices and northwestern locations with lower predicted prices.&lt;/p&gt;

&lt;p&gt;Note: It may be the case that houses in Los Angeles are more expensive than those in San Francisco, and that the geographic location of these cities is driving the pattern we observe. However, this analysis is purely observational, and we are not performing any hypothesis testing or causal inference here.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F7zbubtym4tj6400g1u9r.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%2F7zbubtym4tj6400g1u9r.png" alt=" " width="670" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Waterfall Plot
&lt;/h2&gt;

&lt;p&gt;This plot gives us a per-sample explanation of the model’s prediction, showing how each variable contributed to the final output for a single observation, rather than how a variable behaves across the entire dataset.&lt;/p&gt;

&lt;p&gt;In the plot below, we see how each feature contributes to moving the model’s expected value (the average prediction across all samples, E[f(X)]) to the final prediction for a specific sample (sample_ind).&lt;/p&gt;

&lt;p&gt;Starting from an expected house price of E[f(X)], each variable adds or subtracts from this baseline until we reach the final prediction for that sample, f(x). We can verify this by comparing the model prediction of that specific sample, and the one shown on the plot at f(x):&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;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;sample_ind&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;:])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;array([1.9473549])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the plot, we observe that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Variables shown in blue contribute to pulling the prediction downwards&lt;/li&gt;
&lt;li&gt;Variables shown in red contribute to pushing the prediction upwards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The magnitude of each bar represents how much that feature contributes to the prediction for this specific sample.&lt;/p&gt;

&lt;p&gt;These individual contributions are what we call SHAP values: they quantify how much each feature shifts the prediction away from the baseline E[f(X)] to reach the final output.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waterfall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sample_ind&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;max_display&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fnd1ozg46lfxuzn9mltch.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%2Fnd1ozg46lfxuzn9mltch.png" alt=" " width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: Compare the waterfall plot and the dependence plot, and observe how the SHAP value for the Latitude variable is consistent in both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beeswarm Plot
&lt;/h2&gt;

&lt;p&gt;This plot shows the SHAP values of every variable across all samples. Each point represents a sample, positioned according to its SHAP value (impact on the model output), while the colour indicates the value of the feature (red = high value, blue = low value).&lt;/p&gt;

&lt;p&gt;This allows us to understand both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How the value of a feature influences the prediction&lt;/li&gt;
&lt;li&gt;How common different effects are (denser regions indicate more samples with similar contributions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below is the beeswarm plot for our California housing dataset:&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beeswarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F1q9jqfay992ivc9lwnm0.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%2F1q9jqfay992ivc9lwnm0.png" alt=" " width="744" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Analysing the plot, we can observe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MedInc and AveBedrms:
Both features show a right-skewed pattern in their SHAP values, with a few samples having very large positive contributions. In particular, higher values (red) are associated with strong positive SHAP values, meaning that higher income levels and a larger number of bedrooms tend to significantly increase the predicted house price.
These high-value observations are relatively rare but have a strong influence on the model’s predictions.&lt;/li&gt;
&lt;li&gt;AveOccup and AveRooms:
These features have SHAP values that are mostly concentrated around zero, indicating that for most samples they have a limited impact on the model’s prediction.
However, some high-value outliers (red points) show strong negative SHAP values, meaning that unusually high occupancy or number of rooms can significantly decrease the predicted house price.&lt;/li&gt;
&lt;li&gt;HouseAge and Population:
These features have SHAP values tightly clustered around zero, suggesting they have little to no impact on the model’s predictions overall.&lt;/li&gt;
&lt;li&gt;Latitude (North–South):
There is a clear pattern where higher latitude values (red, more northern locations) tend to have negative SHAP values, meaning they decrease the predicted house price.
Lower latitude values (blue, more southern locations) tend to have positive SHAP values, increasing the prediction.
This suggests that, in our dataset, houses further north tend to be cheaper, while those further south tend to be more expensive.&lt;/li&gt;
&lt;li&gt;Longitude (East–West):
We observe two main clusters of values. Lower longitude values (blue, more western locations) tend to have positive SHAP values, while higher longitude values (red, more eastern locations) tend to have negative SHAP values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This Latitude-Longitude behaviour is consistent with the geographic distribution of the main population centres in California. If we plot the data on a map, we can clearly see two dense clusters: one in the northwest (San Francisco area) and one in the southeast (Los Angeles area).&lt;/p&gt;

&lt;p&gt;Using a density map:&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;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&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;Price&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;y&lt;/span&gt;
&lt;span class="n"&gt;meanlat&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;Latitude&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;meanlon&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;Longitude&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;fig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;density_map&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;lat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Longitude&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;center&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;meanlat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;meanlon&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                    &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;map_style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open-street-map&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;&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%2Fkgixwwe29i28qc9kyrar.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%2Fkgixwwe29i28qc9kyrar.png" alt=" " width="541" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  LightGBM (Explainable model)
&lt;/h1&gt;

&lt;p&gt;Now we switch to a more complex model, one based on gradient boosting: LightGBM, which should be able to detect more hidden and non-linear patterns in our dataset. To quickly find optimum hyperparameters, we will use the "Optuna" automatic hyperparameter optimisation framework for ML.&lt;/p&gt;

&lt;p&gt;We see that performance metrics have improved with the capacity of this model to capture non-linear patterns and interactions between features.&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;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_verbosity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;best_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;best_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-inf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;best_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;

    &lt;span class="n"&gt;train_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&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="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;free_raw_data&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;valid_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_valid&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="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;train_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;free_raw_data&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;param&lt;/span&gt; &lt;span class="o"&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;objective&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;regression&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;metric&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;mean_squared_error&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;boosting_type&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;gbdt&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;verbosity&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="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;boosting_type&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;gbdt&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;lambda_l1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lambda_l1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&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;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&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;num_leaves&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;num_leaves&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;feature_fraction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;feature_fraction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bagging_fraction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bagging_fraction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bagging_freq&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bagging_freq&lt;/span&gt;&lt;span class="sh"&gt;'&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="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;min_child_samples&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;min_child_samples&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;train_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_sets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;valid_data&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_evaluation&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="n"&gt;preds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;r2_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;best_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;
        &lt;span class="n"&gt;best_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;


&lt;span class="n"&gt;study&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_study&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;maximize&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;optimize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_trials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;best_model&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Model performance metrics:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;preds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_valid&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r2 score:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;r2_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preds&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mae:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean_absolute_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model performance metrics:

r2 score: 0.8105733216454589
mae: 0.39733995113249015
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;explainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Explainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;shap_values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;explainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ExactExplainer explainer: 901it [00:13, 16.52it/s]                         
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Dependence plot (+ ICE lines).
&lt;/h2&gt;

&lt;p&gt;With this new model, we observe a richer and more complex relationship between Latitude and house prices in California.&lt;/p&gt;

&lt;p&gt;There is a clear distinction across different latitude ranges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For lower latitude values (southern regions, around Los Angeles), the ICE lines tend to lie above the baseline, indicating a consistent positive contribution to the predicted house prices.&lt;/li&gt;
&lt;li&gt;For higher latitude values (northern regions, around and above San Francisco), the ICE lines tend to lie below the baseline, indicating a negative contribution to the predicted prices.&lt;/li&gt;
&lt;li&gt;In the intermediate range of latitude, the lines cluster around the baseline, suggesting that houses in this region have little to no impact on the prediction, keeping prices close to the dataset average.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, compared to the linear model, this plot shows that the effect of Latitude is no longer strictly linear, but varies depending on the region, capturing more nuanced geographic patterns.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partial_dependence_plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;X100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ice&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="c1"&gt;# Change to false to see only general trend
&lt;/span&gt;    &lt;span class="n"&gt;model_expected_value&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;feature_expected_value&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;shap_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sample_ind&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sample_ind&lt;/span&gt; &lt;span class="o"&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fs2gjep33dl4urcdfs2og.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%2Fs2gjep33dl4urcdfs2og.png" alt=" " width="611" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Scatter Plot.
&lt;/h2&gt;

&lt;p&gt;Once again, we get a clearer view of the non-linearities and interactions in the data. In this scatter plot, SHAP identifies Longitude as the feature most strongly related to the SHAP values of Latitude, and uses it to colour the points.&lt;/p&gt;

&lt;p&gt;From the plot, we observe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Points in the southeast (low latitude, high longitude – shown in red) are more tightly clustered and have a positive contribution to house prices.&lt;/li&gt;
&lt;li&gt;Points in the northwest (high latitude, low longitude – shown in blue) are more spread out and have a negative contribution to house prices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also see clear non-linear transitions:&lt;/p&gt;

&lt;p&gt;Around latitude 34–35, the contribution of Latitude shifts from mostly positive to neutral/negative&lt;br&gt;
Around latitude 38, there is a sharper drop, after which Latitude has a strong negative impact on predicted prices&lt;/p&gt;

&lt;p&gt;There is a small region around latitude ~38 where some points show a slight positive contribution, but overall, the dominant effect in that range is negative.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F7ifmc49zawqx9iftp9yi.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%2F7ifmc49zawqx9iftp9yi.png" alt=" " width="694" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Waterfall Plot.
&lt;/h2&gt;

&lt;p&gt;In the waterfall plot, we see that although the direction of influence of most variables remains similar, the magnitude of their contributions changes significantly compared to the linear model.&lt;/p&gt;

&lt;p&gt;In this case, we observe that the influence of Latitude and Longitude, which previously dominated the prediction, is now more distributed across other variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latitude still has a positive contribution, but its effect is noticeably smaller than in the linear model&lt;/li&gt;
&lt;li&gt;Longitude still has a negative contribution, but its magnitude is also reduced, and it is no longer the second most influential variable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, this model captures effects that the linear regression was not able to identify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average house occupancy (AveOccup) now shows a strong negative contribution for this sample, which was much weaker in the linear model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, the model spreads the contribution across more features, reflecting a more complex set of relationships between the inputs and the predicted house price.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waterfall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sample_ind&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;max_display&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fib5cxm5mlyzwugf7pf1n.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%2Fib5cxm5mlyzwugf7pf1n.png" alt=" " width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Beeswarm Plot.
&lt;/h2&gt;

&lt;p&gt;In this beeswarm plot, we observe that the overall direction of influence of the features is broadly consistent with what we saw in the linear regression model, but the distribution of their effects has changed significantly.&lt;/p&gt;

&lt;p&gt;We can see that LightGBM spreads the influence of feature values more evenly across the dataset. Unlike linear regression, we no longer observe a few extreme outliers dominating the predictions. This reflects one of the limitations of linear models, which can be highly sensitive to outliers.&lt;/p&gt;

&lt;p&gt;At the same time, the relative importance of the main variables remains similar, with features like Latitude, MedInc, and Longitude still playing a dominant role.&lt;/p&gt;

&lt;p&gt;However, we now observe that features that previously had little influence in the linear model contribute more meaningfully to the predictions. This is the case for variables such as HouseAge, AveBedrms, and Population, which now show a wider spread of SHAP values.&lt;/p&gt;

&lt;p&gt;Looking more closely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For Population, lower values can now lead to both positive and negative contributions, indicating that its effect depends on the context (i.e. interactions with other features)&lt;/li&gt;
&lt;li&gt;A similar pattern appears in AveRooms and Longitude, where both high and low values can produce different impacts depending on the sample&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This highlights the key difference with linear regression: the model is no longer assigning a single fixed effect to each feature, but instead capturing non-linear relationships and interactions between variables.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beeswarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fi3cqw72w39blqy6cs91m.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%2Fi3cqw72w39blqy6cs91m.png" alt=" " width="744" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Final Remarks
&lt;/h1&gt;

&lt;p&gt;Please bear in mind not to interpret these plots with causality in mind (i.e. do not draw strong conclusions from them). The plots we have seen show how a trained model responds to different inputs, but they are not a faithful representation of reality, nor a direct explanation of what truly happens inside the model. Rather, they provide an external approximation to help us understand behaviour that would otherwise be too complex to interpret.&lt;/p&gt;

&lt;p&gt;Content in this notebook is heavily inspired by the SHAP documentation:&lt;br&gt;
&lt;a href="https://shap.readthedocs.io/en/latest/example_notebooks/overviews/An%20introduction%20to%20explainable%20AI%20with%20Shapley%20values.html" rel="noopener noreferrer"&gt;https://shap.readthedocs.io/en/latest/example_notebooks/overviews/An%20introduction%20to%20explainable%20AI%20with%20Shapley%20values.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a deeper understanding of the theory behind explainability, see:&lt;br&gt;
&lt;a href="https://christophm.github.io/interpretable-ml-book/" rel="noopener noreferrer"&gt;https://christophm.github.io/interpretable-ml-book/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Please feel free to modify the code used in the plots above to analyse other variables in more detail and extract your own conclusions.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>python</category>
    </item>
    <item>
      <title>OMOP Odyssey - AWS HealthLake ( Strait of Messina )</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 23 Jun 2026 17:33:46 +0000</pubDate>
      <link>https://dev.to/intersystems/omop-odyssey-aws-healthlake-strait-of-messina--74g</link>
      <guid>https://dev.to/intersystems/omop-odyssey-aws-healthlake-strait-of-messina--74g</guid>
      <description>&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%2Fj4s067xwn59cnj1xtvga.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%2Fj4s067xwn59cnj1xtvga.png" alt=" " width="799" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Nearline FHIR® Ingestion to InterSystems OMOP from AWS HealthLake&lt;/h2&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%2Fk3at9sdoqok0g9ahy0x4.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%2Fk3at9sdoqok0g9ahy0x4.png" alt=" " width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This part of the &lt;a href="https://community.intersystems.com/smartsearch?search=OMOP+Odyssey" rel="noopener noreferrer"&gt;OMOP Journey&lt;/a&gt;,&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;we reflect before attempting to challenge&amp;nbsp;&lt;em&gt;Scylla&amp;nbsp;&lt;/em&gt;on how fortunate we are that InterSystems OMOP transform is built on the Bulk FHIR Export&amp;nbsp;as the source payload.&amp;nbsp; This opens up &lt;strong&gt;hands off interoperability&lt;/strong&gt; with the InterSystems OMOP transform across several FHIR® vendors, including Amazon Web Services HealthLake.&lt;/p&gt;

&lt;h4&gt;HealthLake Bulk FHIR Export&lt;br&gt;&amp;nbsp;&lt;/h4&gt;

&lt;p&gt;Healthlake supports bulk fhir import/export from the cli or api, the premise is&amp;nbsp;simple and the docs are over exhaustive, we'll save a model the trouble of training on it again and &lt;a href="https://docs.aws.amazon.com/healthlake/latest/devguide/exporting-fhir-data.html" rel="noopener noreferrer"&gt;link&lt;/a&gt; it if interested.&amp;nbsp; The more valuable thing to understand of the heading of this paragraph is the implementation of the&amp;nbsp;&lt;a href="https://github.com/HL7/bulk-data" rel="noopener noreferrer"&gt;bulk fhir export standard&lt;/a&gt; itself.&lt;/p&gt;

&lt;h4&gt;
&lt;br&gt;Nearline?&lt;/h4&gt;

&lt;p&gt;Yeah, only "Nearline" ingestion, as the HealthLake&amp;nbsp;export is the whole data store, and does not have a feature to be incremental. Additionally it does not support a resource based trigger, so it has to be invoked at an interval or via some other means yet to be apparent to me at the resource activity level.&amp;nbsp; Still a great number of ways to poke the export throughout AWS, and without incremental exports you only want it to be triggered inside a tolerable processing window anyway for the whole datastore.&lt;/p&gt;

&lt;h4&gt;The Whole Datastore?&lt;/h4&gt;

&lt;p&gt;Yes, the job exports &lt;em&gt;&lt;strong&gt;all&lt;/strong&gt;&lt;/em&gt; the resources into a flat structure.&amp;nbsp; Though it may not be the cleanest process to import the same data to catch the incremental data, the InterSystems OMOP transform should handle it.&lt;br&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Walkthrough&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trying to make this short and to the point, the illustration below really encapsulates what a that a scheduled lambda can glue these two solutions together and automate your OMOP ingestion.&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%2F3o4z4qgxviodog9m1kme.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%2F3o4z4qgxviodog9m1kme.png" alt=" " width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Step One, AWS: Create Bucket&lt;/h4&gt;

&lt;p&gt;Create a bucket with a few of keys, one is &lt;strong&gt;shared&lt;/strong&gt; with InterSystems OMOP for ingesting into the FHIR Transformation, the others will support the automated ingestion.&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%2F8fcztplk9vpsod2pc890.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%2F8fcztplk9vpsod2pc890.png" alt=" " width="800" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Explanations of the keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;export - landing area for the raw resource ndjson from the job&lt;/li&gt;
&lt;li&gt;from-healthlake-to-intersystems-omop - landing area for the create .zip and integtration point with InterSystems OMOP&lt;/li&gt;
&lt;li&gt;output - job output&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Step Two, InterSystems OMOP&lt;/h4&gt;

&lt;p&gt;Create the Deployment providing the arn of the bucket and the keys from above, ie: `&lt;em&gt;from-healthlake-to-intersystems-omop&lt;/em&gt;` key.&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%2F3v97dosxpsxopatw8vev.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%2F3v97dosxpsxopatw8vev.png" alt=" " width="721" height="803"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Snag the example policy from the post configuration step as indicated and apply it to the bucket in AWS.&amp;nbsp; There are some exhaustive examples of this in a previous post &lt;a href="https://community.intersystems.com/post/omop-odyssey-intersystems-omop-cloud-service-troy" rel="noopener noreferrer"&gt;OMOP Odyssey -&amp;nbsp;InterSystems OMOP Cloud Service (Troy)&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;Step Three, Schedule a HealthLake&amp;nbsp;Export to Expected InterSystems OMOP format 💫&lt;/h4&gt;

&lt;p&gt;The explanation of the flow of things is in the code itself as well, but I will also put it in the explanation in the form of a prompt so maybe you can land in the same spot with your own changes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In python, show me how to start a HealthLake export job, export it to a target location, and poll the status of the job until it is complete, then read all of the ndjson files it creates and into a zip them without the relative path included in the zip and upload it to another location in the same bucket, once the upload is complete, remove the exported files from the export job.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The resulting function and code are the following:&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%2Fe3ancidcpa7y0g6z6a4c.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%2Fe3ancidcpa7y0g6z6a4c.png" alt=" " width="800" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; json
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; boto3
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; uuid
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; boto3
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; zipfile
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; io
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; os
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; time


&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;lambda_handler&lt;/span&gt;&lt;span&gt;(event, context)&lt;/span&gt;:&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Botos&lt;/span&gt;
    s3 = boto3.client(&lt;span class="hljs-string"&gt;'s3'&lt;/span&gt;)
    client = boto3.client(&lt;span class="hljs-string"&gt;'healthlake'&lt;/span&gt;)

    &lt;span class="hljs-comment"&gt;# Vars&lt;/span&gt;
    small_guid = uuid.uuid4().hex[:&lt;span class="hljs-number"&gt;8&lt;/span&gt;]
    bucket_name = &lt;span class="hljs-string"&gt;'intersystems-omop-fhir-bucket'&lt;/span&gt;
    prefix = &lt;span class="hljs-string"&gt;'export/'&lt;/span&gt;  &lt;span class="hljs-comment"&gt;# Make sure it ends with '/'&lt;/span&gt;
    output_zip_key = &lt;span class="hljs-string"&gt;'from-healthlake-to-intersystems-omop/healthlake_ndjson_'&lt;/span&gt; + small_guid + &lt;span class="hljs-string"&gt;'.zip'&lt;/span&gt;
    datastore_id = &lt;span class="hljs-string"&gt;'9ee0e51d987e#ai#8ca487e8e95b1d'&lt;/span&gt;
    response = client.start_fhir_export_job(
        JobName=&lt;span class="hljs-string"&gt;'FHIR2OMOPJob'&lt;/span&gt;,
        OutputDataConfig={
            &lt;span class="hljs-string"&gt;'S3Configuration'&lt;/span&gt;: {
                &lt;span class="hljs-string"&gt;'S3Uri'&lt;/span&gt;: &lt;span class="hljs-string"&gt;'s3://intersystems-omop-fhir-bucket/export/'&lt;/span&gt;,
                &lt;span class="hljs-string"&gt;'KmsKeyId'&lt;/span&gt;: &lt;span class="hljs-string"&gt;'arn:aws:kms:us-east-2:12345:key/54918bec-#ai#-4710-9c18-1a65d0d4590b'&lt;/span&gt;
            }
        },
        DatastoreId=datastore_id,
        DataAccessRoleArn=&lt;span class="hljs-string"&gt;'arn:aws:iam::12345:role/service-role/AWSHealthLake-Export-2-OMOP'&lt;/span&gt;,
        ClientToken=small_guid
    )

    job_id = response[&lt;span class="hljs-string"&gt;'JobId'&lt;/span&gt;]
    print(&lt;span class="hljs-string"&gt;f"Export job started: &lt;span&gt;{job_id}&lt;/span&gt;"&lt;/span&gt;)

    &lt;span class="hljs-comment"&gt;# Step 2: Poll until the job completes&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;while&lt;/span&gt; &lt;span class="hljs-keyword"&gt;True&lt;/span&gt;:
        status_response = client.describe_fhir_export_job(
            DatastoreId=datastore_id,
            JobId=job_id
        )

        status = status_response[&lt;span class="hljs-string"&gt;'ExportJobProperties'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'JobStatus'&lt;/span&gt;]
        print(&lt;span class="hljs-string"&gt;f"Job status: &lt;span&gt;{status}&lt;/span&gt;"&lt;/span&gt;)

        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; status &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; [&lt;span class="hljs-string"&gt;'COMPLETED'&lt;/span&gt;, &lt;span class="hljs-string"&gt;'FAILED'&lt;/span&gt;, &lt;span class="hljs-string"&gt;'CANCELLED'&lt;/span&gt;]:
            &lt;span class="hljs-keyword"&gt;break&lt;/span&gt;
        time.sleep(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)  &lt;span class="hljs-comment"&gt;# wait before polling again&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Step 3: Final result&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; status == &lt;span class="hljs-string"&gt;'COMPLETED'&lt;/span&gt;:
        output_uri = status_response[&lt;span class="hljs-string"&gt;'ExportJobProperties'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'OutputDataConfig'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'S3Configuration'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'S3Uri'&lt;/span&gt;]
        print(&lt;span class="hljs-string"&gt;f"Export completed. Data available at: &lt;span&gt;{output_uri}&lt;/span&gt;"&lt;/span&gt;)

    &lt;span class="hljs-comment"&gt;# Get list of all objects with .ndjson extension under the prefix&lt;/span&gt;
    ndjson_keys = []
    paginator = s3.get_paginator(&lt;span class="hljs-string"&gt;'list_objects_v2'&lt;/span&gt;)
    &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; page &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; paginator.paginate(Bucket=bucket_name, Prefix=prefix):
        &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; obj &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; page.get(&lt;span class="hljs-string"&gt;'Contents'&lt;/span&gt;, []):
            key = obj[&lt;span class="hljs-string"&gt;'Key'&lt;/span&gt;]
            &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; key.endswith(&lt;span class="hljs-string"&gt;'.ndjson'&lt;/span&gt;):
                ndjson_keys.append(key)

    &lt;span class="hljs-comment"&gt;# Create ZIP in memory&lt;/span&gt;
    zip_buffer = io.BytesIO()
    &lt;span class="hljs-keyword"&gt;with&lt;/span&gt; zipfile.ZipFile(zip_buffer, &lt;span class="hljs-string"&gt;'w'&lt;/span&gt;, zipfile.ZIP_DEFLATED) &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; zf:
        &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; key &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; ndjson_keys:
            obj = s3.get_object(Bucket=bucket_name, Key=key)
            file_data = obj[&lt;span class="hljs-string"&gt;'Body'&lt;/span&gt;].read()
            arcname = os.path.basename(key)
            zf.writestr(arcname, file_data)

    zip_buffer.seek(&lt;span class="hljs-number"&gt;0&lt;/span&gt;)

    &lt;span class="hljs-comment"&gt;# Upload ZIP back to S3&lt;/span&gt;
    s3.put_object(
        Bucket=bucket_name,
        Key=output_zip_key,
        Body=zip_buffer.getvalue()
    )
    print(&lt;span class="hljs-string"&gt;f"Created ZIP with &lt;span&gt;{len(ndjson_keys)}&lt;/span&gt; files at s3://&lt;span&gt;{bucket_name}&lt;/span&gt;/&lt;span&gt;{output_zip_key}&lt;/span&gt;"&lt;/span&gt;)
    &lt;span class="hljs-comment"&gt;# Clean up&lt;/span&gt;
    paginator = s3.get_paginator(&lt;span class="hljs-string"&gt;'list_objects_v2'&lt;/span&gt;)
    pages = paginator.paginate(Bucket=bucket_name, Prefix=prefix)

    &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; page &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; pages:
        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; &lt;span class="hljs-string"&gt;'Contents'&lt;/span&gt; &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; page:
            &lt;span class="hljs-comment"&gt;# Exclude the folder marker itself if it exists&lt;/span&gt;
            delete_keys = [
                {&lt;span class="hljs-string"&gt;'Key'&lt;/span&gt;: obj[&lt;span class="hljs-string"&gt;'Key'&lt;/span&gt;]}
                &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; obj &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; page[&lt;span class="hljs-string"&gt;'Contents'&lt;/span&gt;]
                &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; obj[&lt;span class="hljs-string"&gt;'Key'&lt;/span&gt;] != prefix  &lt;span class="hljs-comment"&gt;# protect the folder key (e.g., 'folder1/')&lt;/span&gt;
            ]

            &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; delete_keys:
                s3.delete_objects(Bucket=bucket_name, Delete={&lt;span class="hljs-string"&gt;'Objects'&lt;/span&gt;: delete_keys})
                print(&lt;span class="hljs-string"&gt;f"Deleted &lt;span&gt;{len(delete_keys)}&lt;/span&gt; objects under &lt;span&gt;{prefix}&lt;/span&gt;"&lt;/span&gt;)
        &lt;span class="hljs-keyword"&gt;else&lt;/span&gt;:
            print(&lt;span class="hljs-string"&gt;f"No objects found under &lt;span&gt;{prefix}&lt;/span&gt;"&lt;/span&gt;)
    &lt;span class="hljs-keyword"&gt;else&lt;/span&gt;:
        print(&lt;span class="hljs-string"&gt;f"Export job did not complete successfully. Status: &lt;span&gt;{status}&lt;/span&gt;"&lt;/span&gt;)
    
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; {
        &lt;span class="hljs-string"&gt;'statusCode'&lt;/span&gt;: &lt;span class="hljs-number"&gt;200&lt;/span&gt;,
        &lt;span class="hljs-string"&gt;'body'&lt;/span&gt;: json.dumps(response)
    }


&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This function fires at an interval of about every 10&amp;nbsp;minutes via an EventBridge schedule, this will have to be adjusted to meet your workload characteristics.&lt;br&gt;&amp;nbsp;&lt;/p&gt;

&lt;h4&gt;Step Four, Validate Ingestion&amp;nbsp;✔&lt;/h4&gt;

&lt;p&gt;LGTM! we can see the zips in the ingestion location are successfully getting picked up by the transform in InterSystems OMOP.&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%2Fio90ypyj0wxkh9s1hw30.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%2Fio90ypyj0wxkh9s1hw30.png" alt=" " width="798" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Step Five, Smoke Data&amp;nbsp;✔&lt;/h4&gt;

&lt;p&gt;LGTM! FHIR Organization Resource = OMOPCDM54 care_site.&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%2Fitymg2jrloynn6tbf2yk.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%2Fitymg2jrloynn6tbf2yk.png" alt=" " width="798" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>OMOP Odyssey - GCP Healthcare API Real Time FHIR® to OMOP Transformation ( RealTymus )</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 23 Jun 2026 17:23:50 +0000</pubDate>
      <link>https://dev.to/intersystems/omop-odyssey-gcp-healthcare-api-real-time-fhirr-to-omop-transformation-realtymus--2o9b</link>
      <guid>https://dev.to/intersystems/omop-odyssey-gcp-healthcare-api-real-time-fhirr-to-omop-transformation-realtymus--2o9b</guid>
      <description>&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%2Flr5jz1o924tom5fndntr.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%2Flr5jz1o924tom5fndntr.png" alt=" " width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Real Time FHIR® to OMOP Transformation&lt;/h2&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%2F4ysgejz2i0b8fuo0nk4k.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%2F4ysgejz2i0b8fuo0nk4k.png" alt=" " width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This part of the&amp;nbsp;&lt;a href="https://community.intersystems.com/smartsearch?search=OMOP+Odyssey" rel="noopener noreferrer"&gt;OMOP Journey&lt;/a&gt;,&amp;nbsp;&amp;nbsp;we reflect before attempting to challenge&amp;nbsp;&lt;em&gt;Scylla&amp;nbsp;&lt;/em&gt;on how fortunate we are that InterSystems OMOP transform is built on the Bulk FHIR Export&amp;nbsp;as the source payload.&amp;nbsp; This opens up&amp;nbsp;&lt;strong&gt;hands off interoperability&lt;/strong&gt;&amp;nbsp;with the InterSystems OMOP transform across several FHIR® vendors, this time with the &lt;a href="https://cloud.google.com/healthcare-api?hl=en" rel="noopener noreferrer"&gt;Google Cloud Healthcare API.&lt;/a&gt;&lt;/p&gt;

&lt;h3 id="health-lake-bulk-f-h-i-r-export"&gt;Google Cloud Healthcare API FHIR® Export&lt;/h3&gt;

&lt;p&gt;GCP FHIR® Datastores support&amp;nbsp;bulk fhir import/export from the cli or api, the premise is&amp;nbsp;simple and the docs are over exhaustive, we'll save a model the trouble of training on it again and&amp;nbsp;&lt;a href="https://cloud.google.com/healthcare-api/docs/how-tos/fhir-import-export" rel="noopener noreferrer"&gt;link&lt;/a&gt;&amp;nbsp;it if interested.&amp;nbsp; The more valuable thing to understand of the heading of this paragraph is the implementation of the&amp;nbsp;&lt;a href="https://github.com/HL7/bulk-data" rel="noopener noreferrer"&gt;bulk fhir export standard&lt;/a&gt;&amp;nbsp;itself.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Important differentiators with Google's implementation of the FHIR®&amp;nbsp;Export are namely, Resource Change Notification via Pub/Sub and the ability to specify &lt;a href="https://cloud.google.com/healthcare-api/docs/how-tos/fhir-import-export#incremental-exports" rel="noopener noreferrer"&gt;incremental exports&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id="nearline"&gt;Real Time?&amp;nbsp;⏲&lt;/h3&gt;

&lt;p&gt;Yes! Ill die on this sword I guess.&amp;nbsp; Its not only my rap handle, but the mechanics are definitely there to back a good technical argument to be able to say...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"As a new Organization gets created to FHIR, we transform it, and add it to the InterSystems OMOP CDM in the same stroke as a care_site/location."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;&lt;strong&gt;Walkthrough&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Trying to make this short and to the point and encapsulates how a pub/sub notification coupled with a cloud&amp;nbsp;function can glue these two solutions together and automate your OMOP ingestion at a granular level.&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%2F00h4trni0vru5zg4ng7i.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%2F00h4trni0vru5zg4ng7i.png" alt=" " width="798" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3 id="step-one-a-w-s-create-bucket"&gt;Step One:&amp;nbsp;Wire Up InterSystems OMOP to AWS Bucket&lt;/h3&gt;

&lt;p&gt;This step is becoming a repetitive in posts in this community, so I will go warp speed through the steps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Procure AWS S3 Bucket&lt;/li&gt;
&lt;li&gt;Launch InterSystems OMOP, Add Bucket Configuration&lt;/li&gt;
&lt;li&gt;Eject Policy from InterSystems OMOP Deployment&lt;/li&gt;
&lt;li&gt;Apply Policy to the AWS S3 Bucket&lt;/li&gt;
&lt;/ul&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%2F3sdkcjbh6z6eqrat2x10.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%2F3sdkcjbh6z6eqrat2x10.png" alt=" " width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;&amp;nbsp;&lt;/h4&gt;

&lt;p&gt;I dunno, the steps and image seemed to work out better in my head, but maybe not.&amp;nbsp; Here are the &lt;a href="https://docs.intersystems.com/services/csp/docbook/DocBook.UI.Page.cls?KEY=PAGE_rdp#PAGE_rdp_create" rel="noopener noreferrer"&gt;docs&lt;/a&gt; and here is a more &lt;a href="https://community.intersystems.com/post/omop-odyssey-intersystems-omop-cloud-service-troy" rel="noopener noreferrer"&gt;in depth way&lt;/a&gt; to get this taken care of in this series with better examples.&lt;/p&gt;

&lt;h3 id="step-one-a-w-s-create-bucket"&gt;Step Two:&amp;nbsp;Add Pub/Sub Target in Google Cloud Healthcare API&lt;/h3&gt;

&lt;p&gt;As mentioned previous, a&amp;nbsp;foundational piece to making this work is the super great feature that notifies on Resource changes in the data store.&amp;nbsp; You will find this option on setup in the dialog and is also available post configuration.&amp;nbsp; I typically like to check both options to have as much data in the notification as possible to play with.&amp;nbsp; For instance with Deletes, you can include the deleted resource in the notification as well, really great for EMPI solutions.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&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%2Fccaq8ipbkno4neibkqqo.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%2Fccaq8ipbkno4neibkqqo.png" alt=" " width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3 id="step-one-a-w-s-create-bucket"&gt;Step Three:&amp;nbsp;Cloud Function&amp;nbsp;⭐&lt;/h3&gt;

&lt;p&gt;The cloud function puts in the work, and the SOW for that&amp;nbsp;looks a little bit like this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Listen for FHIR resource change pub/sub notifications&amp;nbsp;of type Organization on the create method, and export the data store incrementally from the time the event fired.&amp;nbsp; Since the export function only supports a GCS target, read in the created export and create fhir export zip file that zips the&amp;nbsp;ndjson files into the root of the zip file and push the created zip file to an aws bucket.&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Re-stating the second&amp;nbsp;feature that makes this especially great, is the ability to export from an specific date and time, meaning we do not need to export the entire dataset.&amp;nbsp; For this we will use the time we received the event, tack a minute or so on it, in hopes the export, import and transform steps will be smaller and of course, more timely.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;br&gt;
realtimefhir2omop.py&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; os, io, json, base64, time, zipfile, datetime
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; requests, boto3
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; google.cloud &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; storage
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; google.auth.transport.requests &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Request
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; google.auth
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; google.auth.transport.requests &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; AuthorizedSession
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; base64
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; functions_framework
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; pathlib
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; textwrap
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; json
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; datetime &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; datetime, timedelta, timezone



&lt;span class="hljs-comment"&gt;# Config&lt;/span&gt;
PROJECT_ID = &lt;span class="hljs-string"&gt;"pidtoo-fhir"&lt;/span&gt;
LOCATION = &lt;span class="hljs-string"&gt;"us-east4"&lt;/span&gt;
DATASET_ID = &lt;span class="hljs-string"&gt;"isc"&lt;/span&gt;
FHIR_STORE_ID = &lt;span class="hljs-string"&gt;"fhir-omop"&lt;/span&gt;
GCS_EXPORT_BUCKET = &lt;span class="hljs-string"&gt;"fhir-export-bucket"&lt;/span&gt;
AWS_BUCKET = &lt;span class="hljs-string"&gt;"intersystems-fhir2omop"&lt;/span&gt;
AWS_REGION = &lt;span class="hljs-string"&gt;"us-east-2"&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Trigger FHIR export&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;trigger_incremental_export&lt;/span&gt;&lt;span&gt;(export_time_iso)&lt;/span&gt;:&lt;/span&gt;
    client = storage.Client()
    bucket = client.bucket(&lt;span class="hljs-string"&gt;"fhir-export-bucket"&lt;/span&gt;)

    blobs = bucket.list_blobs()
    &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; blob &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; blobs:
        print(&lt;span class="hljs-string"&gt;f"Deleting: &lt;span&gt;{blob.name}&lt;/span&gt;"&lt;/span&gt;)
        blob.delete()
    
    credentials, _ = google.auth.default(scopes=[&lt;span class="hljs-string"&gt;"https://www.googleapis.com/auth/cloud-platform"&lt;/span&gt;])
    authed_session = AuthorizedSession(credentials)

    export_uri = &lt;span class="hljs-string"&gt;f"gs://&lt;span&gt;{GCS_EXPORT_BUCKET}&lt;/span&gt;/fhir-export-&lt;span&gt;{int(time.time())}&lt;/span&gt;/"&lt;/span&gt;
    export_uri = &lt;span class="hljs-string"&gt;f"gs://&lt;span&gt;{GCS_EXPORT_BUCKET}&lt;/span&gt;/"&lt;/span&gt;
    url = (
        &lt;span class="hljs-string"&gt;f"https://healthcare.googleapis.com/v1/projects/&lt;span&gt;{PROJECT_ID}&lt;/span&gt;/locations/&lt;span&gt;{LOCATION}&lt;/span&gt;/"&lt;/span&gt;
        &lt;span class="hljs-string"&gt;f"datasets/&lt;span&gt;{DATASET_ID}&lt;/span&gt;/fhirStores/&lt;span&gt;{FHIR_STORE_ID}&lt;/span&gt;:export"&lt;/span&gt;
    )

    body = {
        &lt;span class="hljs-string"&gt;"gcsDestination"&lt;/span&gt;: {&lt;span class="hljs-string"&gt;"uriPrefix"&lt;/span&gt;: export_uri},
        &lt;span class="hljs-string"&gt;"since"&lt;/span&gt;: export_time_iso
    }

    response = authed_session.post(url, json=body)
    print(&lt;span class="hljs-string"&gt;f"Export response: &lt;span&gt;{response.status_code}&lt;/span&gt; - &lt;span&gt;{response.text}&lt;/span&gt;"&lt;/span&gt;)
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; export_uri &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; response.ok &lt;span class="hljs-keyword"&gt;else&lt;/span&gt; &lt;span class="hljs-keyword"&gt;None&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Poll GCS for export results&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;wait_for_ndjson_files&lt;/span&gt;&lt;span&gt;(export_uri_prefix)&lt;/span&gt;:&lt;/span&gt;
    client = storage.Client()
    bucket_name = export_uri_prefix.split(&lt;span class="hljs-string"&gt;"/"&lt;/span&gt;)[&lt;span class="hljs-number"&gt;2&lt;/span&gt;]
    prefix = &lt;span class="hljs-string"&gt;"/"&lt;/span&gt;.join(export_uri_prefix.split(&lt;span class="hljs-string"&gt;"/"&lt;/span&gt;)[&lt;span class="hljs-number"&gt;3&lt;/span&gt;:])
    print(bucket_name)
    print(prefix)

    bucket = client.bucket(bucket_name)
    &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; _ &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; range(&lt;span class="hljs-number"&gt;20&lt;/span&gt;):  &lt;span class="hljs-comment"&gt;# Wait up to ~5 mins&lt;/span&gt;
        blobs = list(bucket.list_blobs(prefix=prefix))
        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; any(blob.name.endswith(&lt;span class="hljs-string"&gt;"Organization"&lt;/span&gt;) &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; blob &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; blobs):
            &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; [blob &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; blob &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; blobs &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; blob.name.endswith(&lt;span class="hljs-string"&gt;"Organization"&lt;/span&gt;)]
        time.sleep(&lt;span class="hljs-number"&gt;5&lt;/span&gt;)
    &lt;span class="hljs-keyword"&gt;raise&lt;/span&gt; TimeoutError(&lt;span class="hljs-string"&gt;"Export files did not appear in GCS within timeout window"&lt;/span&gt;)

&lt;span class="hljs-comment"&gt;# Zip .ndjsons into flat ZIP file&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;create_zip_from_blobs&lt;/span&gt;&lt;span&gt;(blobs, zip_path)&lt;/span&gt;:&lt;/span&gt;
    client = storage.Client()
    &lt;span class="hljs-keyword"&gt;with&lt;/span&gt; zipfile.ZipFile(zip_path, &lt;span class="hljs-string"&gt;'w'&lt;/span&gt;, zipfile.ZIP_DEFLATED) &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; zipf:
        &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; blob &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; blobs:
            data = blob.download_as_bytes()
            fname = os.path.basename(blob.name)
            zipf.writestr(fname + &lt;span class="hljs-string"&gt;".ndjson"&lt;/span&gt;, data)

&lt;span class="hljs-comment"&gt;# Upload ZIP to AWS S3&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;upload_to_s3&lt;/span&gt;&lt;span&gt;(zip_path, s3_key)&lt;/span&gt;:&lt;/span&gt;
    s3 = boto3.client(&lt;span class="hljs-string"&gt;'s3'&lt;/span&gt;, region_name=AWS_REGION)
    s3.upload_file(zip_path, AWS_BUCKET, &lt;span class="hljs-string"&gt;"from_gcp_to_omop"&lt;/span&gt; + s3_key)
    print(&lt;span class="hljs-string"&gt;f"Uploaded &lt;span&gt;{zip_path}&lt;/span&gt; to s3://&lt;span&gt;{AWS_BUCKET}&lt;/span&gt;/from_gcp_to_omop/&lt;span&gt;{s3_key}&lt;/span&gt;"&lt;/span&gt;)


&lt;span class="hljs-comment"&gt;#@functions_framework.cloud_event&lt;/span&gt;
&lt;span class="hljs-comment"&gt;#def mit_grandhack(cloud_event):&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Print out the data from Pub/Sub, to prove that it worked&lt;/span&gt;
&lt;span class="hljs-comment"&gt;#    print(base64.b64decode(cloud_event.data["message"]["data"]))&lt;/span&gt;
&lt;span class="hljs-comment"&gt;#    question = base64.b64decode(cloud_event.data["message"]["data"]).decode()&lt;/span&gt;
&lt;span class="hljs-meta"&gt;@functions_framework.cloud_event&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;receive_pubsub&lt;/span&gt;&lt;span&gt;(cloud_event)&lt;/span&gt;:&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;#envelope = request.get_json()&lt;/span&gt;
    print(cloud_event)
    data = base64.b64decode(cloud_event.data[&lt;span class="hljs-string"&gt;"message"&lt;/span&gt;][&lt;span class="hljs-string"&gt;"data"&lt;/span&gt;]).decode()
    data = cloud_event.data
    print(data)
    print(type(data))
    &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; &lt;span class="hljs-keyword"&gt;not&lt;/span&gt; data:
        &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;"No data"&lt;/span&gt;, &lt;span class="hljs-number"&gt;400&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;#payload = data # json.loads(data)&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;#method = payload.get("protoPayload", {}).get("methodName", "")&lt;/span&gt;
    method = data[&lt;span class="hljs-string"&gt;'message'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'attributes'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'action'&lt;/span&gt;]
    &lt;span class="hljs-comment"&gt;#resource_name = payload.get("protoPayload", {}).get("resourceName", "")&lt;/span&gt;
    resource_name = data[&lt;span class="hljs-string"&gt;'message'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'attributes'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'resourceType'&lt;/span&gt;]
    &lt;span class="hljs-comment"&gt;#timestamp = payload.get("timestamp", "")&lt;/span&gt;
    timestamp = data[&lt;span class="hljs-string"&gt;'message'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'publishTime'&lt;/span&gt;]
    &lt;span class="hljs-comment"&gt;# Input datetime string&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Parse the string to a datetime object&lt;/span&gt;
    dt = datetime.strptime(timestamp, &lt;span class="hljs-string"&gt;"%Y-%m-%dT%H:%M:%S.%fZ"&lt;/span&gt;).replace(tzinfo=timezone.utc)

    &lt;span class="hljs-comment"&gt;# Subtract 5 minutes&lt;/span&gt;
    five_minutes_ago = dt - timedelta(minutes=&lt;span class="hljs-number"&gt;5&lt;/span&gt;)

    &lt;span class="hljs-comment"&gt;# Convert back to ISO 8601 string format with 'Z'&lt;/span&gt;
    timestamp = five_minutes_ago.isoformat().replace(&lt;span class="hljs-string"&gt;'+00:00'&lt;/span&gt;, &lt;span class="hljs-string"&gt;'Z'&lt;/span&gt;)

    print(method)
    print(resource_name)
    print(timestamp)

    &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; &lt;span class="hljs-string"&gt;"CreateResource"&lt;/span&gt; &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; method &lt;span class="hljs-keyword"&gt;and&lt;/span&gt; &lt;span class="hljs-string"&gt;"Organization"&lt;/span&gt; &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; resource_name:
        print(&lt;span class="hljs-string"&gt;f"New Organization detected at &lt;span&gt;{timestamp}&lt;/span&gt;"&lt;/span&gt;)
        export_uri = trigger_incremental_export(timestamp)
        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; &lt;span class="hljs-keyword"&gt;not&lt;/span&gt; export_uri:
            &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;"Export failed"&lt;/span&gt;, &lt;span class="hljs-number"&gt;500&lt;/span&gt;
        blobs = wait_for_ndjson_files(export_uri)
        zip_file_path = &lt;span class="hljs-string"&gt;"/tmp/fhir_export.zip"&lt;/span&gt;
        create_zip_from_blobs(blobs, zip_file_path)
        s3_key = &lt;span class="hljs-string"&gt;f"/export-&lt;span&gt;{int(time.time())}&lt;/span&gt;.zip"&lt;/span&gt;
        upload_to_s3(zip_file_path, s3_key)
        &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;"Exported and uploaded"&lt;/span&gt;, &lt;span class="hljs-number"&gt;200&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;"No relevant event"&lt;/span&gt;, &lt;span class="hljs-number"&gt;204&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;h3 id="step-one-a-w-s-create-bucket"&gt;Step Four: What is Happening right now?&amp;nbsp;🔥&lt;/h3&gt;

&lt;p&gt;To split what is going on, lets inspect the real time processing with some screenshots at each point.&lt;/p&gt;

&lt;h4&gt;FHIR Organization Created&lt;/h4&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%2Feqvuyrl6t8qxjc7odvea.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%2Feqvuyrl6t8qxjc7odvea.png" alt=" " width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Pub/Sub Event is Published&lt;/h4&gt;

&lt;p&gt;&amp;nbsp;&lt;br&gt;
Pub/Sub FHIR Event&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{'attributes': {'specversion': '&lt;span class="hljs-number"&gt;1.0&lt;/span&gt;', 'id': '&lt;span class="hljs-number"&gt;13999883936448345&lt;/span&gt;', 'source': '&lt;span class="hljs-comment"&gt;//pubsub.googleapis.com/projects/pidtoo-fhir/topics/fhir-omop-topic', 'type': 'google.cloud.pubsub.topic.v1.messagePublished', 'datacontenttype': 'application/json', 'time': '2025-05-13T20:13:20.339Z'}, 'data': {'message': {'attributes': {'action': 'CreateResource', 'lastUpdatedTime': 'Tue, 13 May 2025 20:13:20 UTC', 'payloadType': 'FullResource', 'resourceType': 'Organization', 'storeName': 'projects/pidtoo-fhir/locations/us-east4/datasets/isc/fhirStores/fhir-omop', 'versionId': 'MTc0NzE2NzIwMDEwNzczODAwMA'}, 'data': 'ewogICJhZGRyZXNzIjogWwogICAgewogICAgICAiY2l0eSI6IC', 'messageId': '13999883936448345', 'message_id': '13999883936448345', 'publishTime': '2025-05-13T20:13:20.339Z', 'publish_time': '2025-05-13T20:13:20.339Z'}, 'subscription': 'projects/pidtoo-fhir/subscriptions/eventarc-us-east4-fhir2omop-trigger-sub-855'}}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;h4&gt;Cloud Function Receives Resource Event from Subscription&lt;/h4&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%2F3ad7n7bi6o4x01rfzxar.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%2F3ad7n7bi6o4x01rfzxar.png" alt=" " width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Cloud Function Exports the FHIR Store GCS&lt;/h4&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%2Fg9jm6ksz6231azmvg5nv.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%2Fg9jm6ksz6231azmvg5nv.png" alt=" " width="800" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Cloud Function Creates ZIP from GCS and Pushes to AWS&lt;/h4&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%2Fz5sgxh022srca9xg6yoo.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%2Fz5sgxh022srca9xg6yoo.png" alt=" " width="798" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;InterSystems OMOP Transforms FHIR to OMOP&lt;/h4&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%2F5oz0mn2b26tis956uh9u.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%2F5oz0mn2b26tis956uh9u.png" alt=" " width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Organization Available as Care Site in CDM&lt;/h4&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%2Fh1rdwinbzaqnt9xoe01t.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%2Fh1rdwinbzaqnt9xoe01t.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When did that FHIR Resource get transformed to the CDM ?&lt;br&gt;&lt;br&gt;&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%2F1fec7w6peuymijyt4c5i.gif" 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%2F1fec7w6peuymijyt4c5i.gif" alt=" " width="400" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Step Four: Validation Fun&amp;nbsp;✔&lt;/h3&gt;

&lt;p&gt;Fun with OBS and Not so Much fun with Audio&lt;br&gt;&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/-nbYXKQvdQc"&gt;
  &lt;/iframe&gt;
&lt;br&gt;&amp;nbsp;&lt;/p&gt;

&lt;h3&gt;In Conclusion&lt;br&gt;&amp;nbsp;&lt;/h3&gt;

&lt;p&gt;Did something similar last year at MIT Grand Hack, using the same design pattern, but with Questionairre/Response resource and Gemini in the middle of things.&lt;br&gt;&lt;br&gt;&lt;a href="https://www.linkedin.com/pulse/gemini-fhir-agent-ron-sweeney-bk7lc/" rel="noopener noreferrer"&gt;Gemini FHIR Agent MIT Grand Hack&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>analytics</category>
      <category>beginners</category>
      <category>firebase</category>
    </item>
    <item>
      <title>Fast Automatic ML Hyperparameter tuning Using Optuna (w. MLflow model registry and IRIS DB)</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 16 Jun 2026 15:37:19 +0000</pubDate>
      <link>https://dev.to/intersystems/fast-automatic-ml-hyperparameter-tuning-using-optuna-w-mlflow-model-registry-and-iris-db-5aoc</link>
      <guid>https://dev.to/intersystems/fast-automatic-ml-hyperparameter-tuning-using-optuna-w-mlflow-model-registry-and-iris-db-5aoc</guid>
      <description>&lt;p&gt;This article presents a straightforward approach to automatically and efficiently tune hyperparameters for machine learning models using Optuna as the optimisation framework. We explore how to use both Optuna’s native storage options and InterSystems IRIS as a database backend to track the progress of hyperparameter searches. We also show how MLflow can be used to monitor experiments and manage models through its tracking and model registry UI.&lt;/p&gt;

&lt;p&gt;This article is based on this &lt;a href="https://www.kaggle.com/code/jorgeivnjh/fast-automatic-ml-hyperparameter-tuning-w-optuna" rel="noopener noreferrer"&gt;Kaggle Notebook&lt;/a&gt;, which you can run and directly edit yourself.&lt;/p&gt;

&lt;p&gt;When training ML models, the choice of hyperparameters can strongly influence performance. They are not the only factor, but they can significantly affect both convergence and generalisation.&lt;/p&gt;

&lt;p&gt;Tuning hyperparameters manually takes a lot of effort. This is especially true because hyperparameters interact with each other, so tuning them independently is usually not enough. For example, higher regularisation may require a lower learning rate for more stable optimization. A more complex model may require stronger regularization to avoid overfitting, but at the same time, a very small learning rate on a complex model can make learning too slow.&lt;/p&gt;

&lt;p&gt;Optuna is an MIT-licensed open source library, which allows commercial use, that automates hyperparameter search for ML models developed with the most popular frameworks such as scikit-learn, PyTorch, TensorFlow, and LightGBM. It works by defining a search space and an objective metric to either minimize or maximize. Optuna then explores the search space efficiently to find well-performing configurations.&lt;/p&gt;

&lt;p&gt;Here we use Optuna to tune a LightGBM model on a dummy dataset and show how to scale the search using shared database storage. We will also use MLflow for experiment tracking and model registry, and IRIS DB as a possible Optuna storage backend for concurrent studies.&lt;/p&gt;

&lt;p&gt;We will use the California Housing dataset, commonly used in ML examples, to populate IRIS tables and run the tuning workflow.&lt;/p&gt;

&lt;p&gt;Note: For the last bit, you will need an existing IRIS instance that you can connect to. I am using the one created with Docker by running the docker-compose file from this &lt;a href="https://github.com/JorgeIvanJH/IRIS_and_MLflow-Continuous-Training-Pipeline" rel="noopener noreferrer"&gt;repo&lt;/a&gt;. I am also using the environment variables and requirements.txt from that repository, together with Python 3.12.&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;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&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;sqlalchemy&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_engine&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lightgbm&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cross_val_score&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KFold&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;seaborn&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sns&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;seaborn&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sns&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;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Connection String to Existing IRIS Database
&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IRIS_SERVER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IRIS_PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Standard InterSystems superserver port
&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IRIS_NAMESPACE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IRIS_USERNAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IRIS_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pandas version: &lt;/span&gt;&lt;span class="si"&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;__version__&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="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;sklearn version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&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="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;sqlalchemy version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sqlalchemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&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="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;optuna version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&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="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;lightgbm version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&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="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;seaborn version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&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="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;matplotlib version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matplotlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pandas version: 2.3.3
sklearn version: 1.8.0
sqlalchemy version: 2.0.46
optuna version: 4.8.0
lightgbm version: 4.6.0
seaborn version: 0.13.2
matplotlib version: 3.10.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick Intro to Optuna
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://optuna.org/" rel="noopener noreferrer"&gt;Optuna&lt;/a&gt; is a hyperparameter optimization framework that speeds up tuning by training multiple model configurations and learning from their results. It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Efficient sampling strategies, such as TPE, to focus on promising regions of the search space&lt;/li&gt;
&lt;li&gt;Pruning strategies to stop unpromising trials early&lt;/li&gt;
&lt;li&gt;Support for distributed optimization through shared storage&lt;/li&gt;
&lt;li&gt;Visualization tools to understand the search space and parameter importance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a richer intro to Optuna, see this &lt;a href="https://www.youtube.com/watch?v=P6NwZVl8ttc" rel="noopener noreferrer"&gt;video&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Optuna to Avoid Endless Hyperparameter Tuning:
&lt;/h3&gt;

&lt;p&gt;A practical approach to efficiently find good hyperparameters is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run an initial broad search to identify reasonable ranges and baseline parameters. In a CT pipeline, this would usually happen during the experimentation phase.&lt;/li&gt;
&lt;li&gt;Run a more focused Optuna search over the most promising ranges. In a CT pipeline, this can be repeated when there is data drift, model degradation, or a significant change in the dataset.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Important! Hyperparameter tuning must use an appropriate validation setup. Otherwise, we may only find the configuration that best overfits the validation split, rather than one that generalizes well to the dataset at hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading Dataset
&lt;/h2&gt;

&lt;p&gt;The cell below loads scikit-learn's fetch_california_housing dataset, and changes the column names to snake case.&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="c1"&gt;# Load California Housing Dataset
&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datasets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_california_housing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;return_X_y&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;as_frame&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;X&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;col&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;median_house_value&lt;/span&gt;&lt;span class="sh"&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;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&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;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Model Definition and Training
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Choosing the right K-fold Split
&lt;/h3&gt;

&lt;p&gt;It is essential to choose the right cross-validation strategy. This depends on the task, whether it is regression or classification, whether the target is imbalanced, whether the order of samples matters, and whether there are groups in the data. For example, if multiple rows belong to the same patient, we may want to avoid having samples from the same patient appear in both training and validation splits.&lt;/p&gt;

&lt;p&gt;Refer to this &lt;a href="https://scikit-learn.org/stable/auto_examples/model_selection/plot_cv_indices.html#sphx-glr-auto-examples-model-selection-plot-cv-indices-py" rel="noopener noreferrer"&gt;summary&lt;/a&gt; of the options available in SKlearn for further guidance.&lt;/p&gt;

&lt;p&gt;For simplicity, we can use the following decision rules:&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;if&lt;/span&gt; &lt;span class="n"&gt;time_order_matters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;TimeSeriesSplit&lt;/span&gt;   &lt;span class="c1"&gt;# no shuffle equivalent
&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;groups_exist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;classification&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;classes_are_imbalanced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;StratifiedGroupKFold&lt;/span&gt;   &lt;span class="c1"&gt;# (no shuffle equivalent)
&lt;/span&gt;        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;GroupKFold&lt;/span&gt;             &lt;span class="c1"&gt;# → or GroupShuffleSplit
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;classification&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;classes_are_imbalanced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;StratifiedKFold&lt;/span&gt;        &lt;span class="c1"&gt;# → or StratifiedShuffleSplit
&lt;/span&gt;        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;KFold&lt;/span&gt;                  &lt;span class="c1"&gt;# → or ShuffleSplit
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;crossvalstrategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KFold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_splits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shuffle&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;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hyperparemeter Search with Optuna
&lt;/h3&gt;

&lt;p&gt;After choosing the model, in this case LightGBM, we define the hyperparameters that we want to tune and the metric that we want to optimize.&lt;/p&gt;

&lt;p&gt;The cells in this section can be run multiple times until we reach a satisfactory performance level. The variables marked as tweakable are the ones we are likely to adjust between studies.&lt;/p&gt;

&lt;p&gt;The general process is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run an initial study with a broad search space.&lt;/li&gt;
&lt;li&gt;Inspect the best trials, parameter importance, and search-space plots.&lt;/li&gt;
&lt;li&gt;Use those results to define narrower and more promising ranges.&lt;/li&gt;
&lt;li&gt;Run a new study over the refined search space.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since this is a regression task, we use mean squared error as the metric to minimize. The metric is evaluated using the cross-validation strategy defined above.&lt;/p&gt;

&lt;p&gt;Note: When storage=storage_url points to a supported database, such as SQLite or InterSystems IRIS, Optuna automatically creates the tables needed to track studies, trials, parameters, and results. Each study is identified by its study_name. If the same study name and database are reused with load_if_exists=True, Optuna resumes from the existing study instead of starting from scratch.&lt;/p&gt;

&lt;p&gt;This shared storage is also what enables concurrent optimization: multiple processes, or even multiple machines, can connect to the same database and contribute trials to the same study.&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;NUM_TRIALS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="c1"&gt;# Tweak
&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOKY_MAX_CPU_COUNT&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="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpu_count&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="o"&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;learning_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;learning_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;log&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="c1"&gt;# Tweak
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;# Tweak
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n_estimators&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&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_estimators&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;# Tweak
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_leaves&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_categorical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_leaves&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="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&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="c1"&gt;# Tweak
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_categorical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_bin&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="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LGBMRegressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                            &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;crossvalstrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                            &lt;span class="n"&gt;scoring&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;neg_mean_squared_error&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_jobs&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;scores&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;study&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_study&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;study_name&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;lightgbm_hyperparam_tuning_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d_%H-%M-%S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&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;direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minimize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="c1"&gt;# storage=storage_url,
&lt;/span&gt;                            &lt;span class="n"&gt;load_if_exists&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;sampler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;samplers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TPESampler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;),)&lt;/span&gt;
&lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;optimize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_trials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NUM_TRIALS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;show_progress_bar&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;n_jobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;best_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_params&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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Best parameters: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;best_params&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="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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Best performance: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_value&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[32m[I 2026-05-13 15:58:38,618][0m A new study created in memory with name: lightgbm_hyperparam_tuning_2026-05-13_15-58-38[0m



  0%|          | 0/20 [00:00&amp;lt;?, ?it/s]


[32m[I 2026-05-13 15:59:02,770][0m Trial 0 finished with value: 0.22124664870518 and parameters: {'learning_rate': 0.00727491708802781, 'max_depth': 48, 'n_estimators': 746, 'num_leaves': 255, 'lambda_l2': 0.002570603566117598, 'max_bin': 255}. Best is trial 0 with value: 0.22124664870518.[0m
[32m[I 2026-05-13 15:59:06,986][0m Trial 1 finished with value: 0.2059125561807643 and parameters: {'learning_rate': 0.0823143373099555, 'max_depth': 13, 'n_estimators': 222, 'num_leaves': 63, 'lambda_l2': 0.0032112643094417484, 'max_bin': 255}. Best is trial 1 with value: 0.2059125561807643.[0m
[32m[I 2026-05-13 15:59:13,470][0m Trial 2 finished with value: 0.25714400572802726 and parameters: {'learning_rate': 0.01120548642504815, 'max_depth': 40, 'n_estimators': 239, 'num_leaves': 127, 'lambda_l2': 3.850031979199519e-08, 'max_bin': 127}. Best is trial 1 with value: 0.2059125561807643.[0m
[32m[I 2026-05-13 15:59:22,415][0m Trial 3 finished with value: 0.26413921215873515 and parameters: {'learning_rate': 0.0050225633119947675, 'max_depth': 7, 'n_estimators': 700, 'num_leaves': 255, 'lambda_l2': 2.133142332373004e-06, 'max_bin': 63}. Best is trial 1 with value: 0.2059125561807643.[0m
[32m[I 2026-05-13 15:59:28,245][0m Trial 4 finished with value: 0.20942294704047681 and parameters: {'learning_rate': 0.01811326544803337, 'max_depth': 11, 'n_estimators': 972, 'num_leaves': 31, 'lambda_l2': 6.257956190096665e-08, 'max_bin': 255}. Best is trial 1 with value: 0.2059125561807643.[0m
[32m[I 2026-05-13 15:59:54,053][0m Trial 5 finished with value: 0.22529793459324102 and parameters: {'learning_rate': 0.007840758945457348, 'max_depth': 16, 'n_estimators': 838, 'num_leaves': 255, 'lambda_l2': 4.6876566400928895e-08, 'max_bin': 63}. Best is trial 1 with value: 0.2059125561807643.[0m
[32m[I 2026-05-13 15:59:57,575][0m Trial 6 finished with value: 0.6243686001512612 and parameters: {'learning_rate': 0.0010296901472345186, 'max_depth': 42, 'n_estimators': 722, 'num_leaves': 31, 'lambda_l2': 0.5860448217200517, 'max_bin': 63}. Best is trial 1 with value: 0.2059125561807643.[0m
[32m[I 2026-05-13 16:00:01,328][0m Trial 7 finished with value: 0.25616396880444836 and parameters: {'learning_rate': 0.005194929407101736, 'max_depth': 18, 'n_estimators': 743, 'num_leaves': 31, 'lambda_l2': 0.0703178263660987, 'max_bin': 127}. Best is trial 1 with value: 0.2059125561807643.[0m
[32m[I 2026-05-13 16:00:02,230][0m Trial 8 finished with value: 0.4328137375744699 and parameters: {'learning_rate': 0.015952322469109693, 'max_depth': 23, 'n_estimators': 74, 'num_leaves': 63, 'lambda_l2': 1.4726456718740824, 'max_bin': 255}. Best is trial 1 with value: 0.2059125561807643.[0m
[32m[I 2026-05-13 16:00:03,606][0m Trial 9 finished with value: 0.5036899804922363 and parameters: {'learning_rate': 0.0033610226697378754, 'max_depth': 6, 'n_estimators': 325, 'num_leaves': 31, 'lambda_l2': 0.1710207048797339, 'max_bin': 127}. Best is trial 1 with value: 0.2059125561807643.[0m
[32m[I 2026-05-13 16:00:07,940][0m Trial 10 finished with value: 0.21142577467959092 and parameters: {'learning_rate': 0.14804113057514628, 'max_depth': 30, 'n_estimators': 458, 'num_leaves': 63, 'lambda_l2': 3.757350306893132e-05, 'max_bin': 255}. Best is trial 1 with value: 0.2059125561807643.[0m
[32m[I 2026-05-13 16:00:11,156][0m Trial 11 finished with value: 0.2017814916171883 and parameters: {'learning_rate': 0.08309297264998405, 'max_depth': 12, 'n_estimators': 950, 'num_leaves': 16, 'lambda_l2': 0.0008326596975497944, 'max_bin': 255}. Best is trial 11 with value: 0.2017814916171883.[0m
[32m[I 2026-05-13 16:00:12,488][0m Trial 12 finished with value: 0.20764432653610213 and parameters: {'learning_rate': 0.10507813096831281, 'max_depth': 28, 'n_estimators': 508, 'num_leaves': 16, 'lambda_l2': 0.0016316751769423123, 'max_bin': 255}. Best is trial 11 with value: 0.2017814916171883.[0m
[32m[I 2026-05-13 16:00:12,862][0m Trial 13 finished with value: 0.3044026543083153 and parameters: {'learning_rate': 0.054273532006916266, 'max_depth': 3, 'n_estimators': 131, 'num_leaves': 16, 'lambda_l2': 6.119264662645272e-05, 'max_bin': 255}. Best is trial 11 with value: 0.2017814916171883.[0m
[32m[I 2026-05-13 16:00:16,388][0m Trial 14 finished with value: 0.20646055020810183 and parameters: {'learning_rate': 0.041057846227823123, 'max_depth': 14, 'n_estimators': 366, 'num_leaves': 63, 'lambda_l2': 0.007230065446525416, 'max_bin': 255}. Best is trial 11 with value: 0.2017814916171883.[0m
[32m[I 2026-05-13 16:00:18,008][0m Trial 15 finished with value: 0.21268042685192567 and parameters: {'learning_rate': 0.04807456550053136, 'max_depth': 21, 'n_estimators': 604, 'num_leaves': 16, 'lambda_l2': 6.458243615671745e-06, 'max_bin': 255}. Best is trial 11 with value: 0.2017814916171883.[0m
[32m[I 2026-05-13 16:00:28,022][0m Trial 16 finished with value: 0.21844697644015332 and parameters: {'learning_rate': 0.18423283160212306, 'max_depth': 10, 'n_estimators': 992, 'num_leaves': 127, 'lambda_l2': 9.015211997542714, 'max_bin': 255}. Best is trial 11 with value: 0.2017814916171883.[0m
[32m[I 2026-05-13 16:00:29,373][0m Trial 17 finished with value: 0.20797590828555537 and parameters: {'learning_rate': 0.08294987485804219, 'max_depth': 33, 'n_estimators': 188, 'num_leaves': 63, 'lambda_l2': 0.018231434623139052, 'max_bin': 255}. Best is trial 11 with value: 0.2017814916171883.[0m
[32m[I 2026-05-13 16:00:30,247][0m Trial 18 finished with value: 0.23633039578627624 and parameters: {'learning_rate': 0.02831149820738454, 'max_depth': 24, 'n_estimators': 355, 'num_leaves': 16, 'lambda_l2': 0.00012197971292668617, 'max_bin': 127}. Best is trial 11 with value: 0.2017814916171883.[0m
[32m[I 2026-05-13 16:00:35,660][0m Trial 19 finished with value: 0.21720640666066582 and parameters: {'learning_rate': 0.07858633974467637, 'max_depth': 13, 'n_estimators': 879, 'num_leaves': 63, 'lambda_l2': 0.0007188574432995588, 'max_bin': 63}. Best is trial 11 with value: 0.2017814916171883.[0m

Best parameters: {'learning_rate': 0.08309297264998405, 'max_depth': 12, 'n_estimators': 950, 'num_leaves': 16, 'lambda_l2': 0.0008326596975497944, 'max_bin': 255}

Best performance: 0.2017814916171883
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below we inspect the best-performing trials from the study. This gives us a quick view of which hyperparameter combinations performed best and helps guide future searches:&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;trials_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trials_dataframe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;trials_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trials_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;trials_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trials_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;trials_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&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;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params|value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;top_trials_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trials_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;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_trials_df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_trials_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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;th&gt;params_lambda_l2&lt;/th&gt;
&lt;th&gt;params_learning_rate&lt;/th&gt;
&lt;th&gt;params_max_bin&lt;/th&gt;
&lt;th&gt;params_max_depth&lt;/th&gt;
&lt;th&gt;params_n_estimators&lt;/th&gt;
&lt;th&gt;params_num_leaves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;0.201781&lt;/td&gt;
&lt;td&gt;8.326597e-04&lt;/td&gt;
&lt;td&gt;0.083093&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;950&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.205913&lt;/td&gt;
&lt;td&gt;3.211264e-03&lt;/td&gt;
&lt;td&gt;0.082314&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;222&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;0.206461&lt;/td&gt;
&lt;td&gt;7.230065e-03&lt;/td&gt;
&lt;td&gt;0.041058&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;366&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;0.207644&lt;/td&gt;
&lt;td&gt;1.631675e-03&lt;/td&gt;
&lt;td&gt;0.105078&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;508&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;0.207976&lt;/td&gt;
&lt;td&gt;1.823143e-02&lt;/td&gt;
&lt;td&gt;0.082950&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;188&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0.209423&lt;/td&gt;
&lt;td&gt;6.257956e-08&lt;/td&gt;
&lt;td&gt;0.018113&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;972&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.211426&lt;/td&gt;
&lt;td&gt;3.757350e-05&lt;/td&gt;
&lt;td&gt;0.148041&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;458&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;0.212680&lt;/td&gt;
&lt;td&gt;6.458244e-06&lt;/td&gt;
&lt;td&gt;0.048075&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;604&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;0.217206&lt;/td&gt;
&lt;td&gt;7.188574e-04&lt;/td&gt;
&lt;td&gt;0.078586&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;879&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0.218447&lt;/td&gt;
&lt;td&gt;9.015212e+00&lt;/td&gt;
&lt;td&gt;0.184233&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;992&lt;/td&gt;
&lt;td&gt;127&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;th&gt;params_lambda_l2&lt;/th&gt;
&lt;th&gt;params_learning_rate&lt;/th&gt;
&lt;th&gt;params_max_bin&lt;/th&gt;
&lt;th&gt;params_max_depth&lt;/th&gt;
&lt;th&gt;params_n_estimators&lt;/th&gt;
&lt;th&gt;params_num_leaves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;count&lt;/td&gt;
&lt;td&gt;10.000000&lt;/td&gt;
&lt;td&gt;1.000000e+01&lt;/td&gt;
&lt;td&gt;10.000000&lt;/td&gt;
&lt;td&gt;10.000000&lt;/td&gt;
&lt;td&gt;10.000000&lt;/td&gt;
&lt;td&gt;10.000000&lt;/td&gt;
&lt;td&gt;10.000000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mean&lt;/td&gt;
&lt;td&gt;0.209896&lt;/td&gt;
&lt;td&gt;9.047112e-01&lt;/td&gt;
&lt;td&gt;0.087154&lt;/td&gt;
&lt;td&gt;235.800000&lt;/td&gt;
&lt;td&gt;18.500000&lt;/td&gt;
&lt;td&gt;613.900000&lt;/td&gt;
&lt;td&gt;52.100000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std&lt;/td&gt;
&lt;td&gt;0.005155&lt;/td&gt;
&lt;td&gt;2.849745e+00&lt;/td&gt;
&lt;td&gt;0.049444&lt;/td&gt;
&lt;td&gt;60.715731&lt;/td&gt;
&lt;td&gt;8.759122&lt;/td&gt;
&lt;td&gt;313.844424&lt;/td&gt;
&lt;td&gt;34.252169&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;min&lt;/td&gt;
&lt;td&gt;0.201781&lt;/td&gt;
&lt;td&gt;6.257956e-08&lt;/td&gt;
&lt;td&gt;0.018113&lt;/td&gt;
&lt;td&gt;63.000000&lt;/td&gt;
&lt;td&gt;10.000000&lt;/td&gt;
&lt;td&gt;188.000000&lt;/td&gt;
&lt;td&gt;16.000000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;0.206756&lt;/td&gt;
&lt;td&gt;2.078945e-04&lt;/td&gt;
&lt;td&gt;0.055703&lt;/td&gt;
&lt;td&gt;255.000000&lt;/td&gt;
&lt;td&gt;12.250000&lt;/td&gt;
&lt;td&gt;389.000000&lt;/td&gt;
&lt;td&gt;19.750000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;0.208699&lt;/td&gt;
&lt;td&gt;1.232167e-03&lt;/td&gt;
&lt;td&gt;0.082632&lt;/td&gt;
&lt;td&gt;255.000000&lt;/td&gt;
&lt;td&gt;13.500000&lt;/td&gt;
&lt;td&gt;556.000000&lt;/td&gt;
&lt;td&gt;63.000000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;0.212367&lt;/td&gt;
&lt;td&gt;6.225365e-03&lt;/td&gt;
&lt;td&gt;0.099582&lt;/td&gt;
&lt;td&gt;255.000000&lt;/td&gt;
&lt;td&gt;26.250000&lt;/td&gt;
&lt;td&gt;932.250000&lt;/td&gt;
&lt;td&gt;63.000000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max&lt;/td&gt;
&lt;td&gt;0.218447&lt;/td&gt;
&lt;td&gt;9.015212e+00&lt;/td&gt;
&lt;td&gt;0.184233&lt;/td&gt;
&lt;td&gt;255.000000&lt;/td&gt;
&lt;td&gt;33.000000&lt;/td&gt;
&lt;td&gt;992.000000&lt;/td&gt;
&lt;td&gt;127.000000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the first broad search, we can estimate which hyperparameters had the strongest impact on performance. This helps us decide which parameters deserve a more focused search in the next study.&lt;/p&gt;

&lt;p&gt;The cell below calculates the importance score for each hyperparameter on a scale from 0 to 1. Higher values indicate parameters that had more influence on the objective metric in this study.&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;param_importance_dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;importance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_param_importances&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;study&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;figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;sns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;barplot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param_importance_dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param_importance_dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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;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;Importance Score&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;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;Hyperparameter&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;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;Hyperparameter Importance&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;grid&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;&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.amazonaws.com%2Fuploads%2Farticles%2Ff2y37we7351tp19208ew.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.amazonaws.com%2Fuploads%2Farticles%2Ff2y37we7351tp19208ew.png" alt=" " width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the plot above, we can identify the most relevant hyperparameters. Next, we choose how many of the top parameters we want to compare. In this example, we select the two most important ones.&lt;/p&gt;

&lt;p&gt;The contour plot below helps us visualize how these two parameters interact and which regions of the search space produced better results. We can use this to define narrower ranges for future studies.&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;numparamstocompare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;best2params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param_importance_dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&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;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&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="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;numparamstocompare&lt;/span&gt;&lt;span class="p"&gt;:]]&lt;/span&gt;
&lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visualization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matplotlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plot_contour&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;best2params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fqcidul5pd61ethdcfyqm.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.amazonaws.com%2Fuploads%2Farticles%2Fqcidul5pd61ethdcfyqm.png" alt=" " width="592" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  &amp;nbsp;
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Concurrent studies to speed up Hyperparameter exploration
&lt;/h1&gt;

&lt;p&gt;Every time we test a set of hyperparameters, we should evaluate it properly using cross-validation to avoid selecting a model that just overfits to a particular train/validation split. This means training as many models as the number of folds we choose.&lt;/p&gt;

&lt;p&gt;For example, using 5-fold or 10-fold cross-validation implies training 5–10 models per hyperparameter configuration. There is no strict rule for the number of folds, but 5 or 10 are commonly used depending on how expensive each model is to train. As a result, evaluating each set of hyperparameters becomes 5–10 times more time-consuming, and this cost increases further as the dataset grows.&lt;/p&gt;

&lt;p&gt;For this reason, we want to accelerate the hyperparameter search. One way to do this is by running multiple processes, each working on the same Optuna study and exploring the same search space in parallel. If a machine has 16 cores, we can run up to 16 workers concurrently, which can significantly reduce the total optimization time (although not always perfectly linearly due to overhead and coordination between workers).&lt;/p&gt;

&lt;p&gt;An important advantage of Optuna is that if all workers point to a common storage database, the study is shared across processes. Optuna will create and manage the required tables in the database, and all workers will contribute trials to the same study. This means that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workers generally avoid evaluating identical hyperparameter configurations&lt;/li&gt;
&lt;li&gt;Completed trials from all workers are used to guide future sampling&lt;/li&gt;
&lt;li&gt;The search becomes more efficient over time as more results are collected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default, you can specify "sqlite:///optuna_lgbm.db" as the storage parameter, and Optuna will create a local database for the study. The same approach can also be extended to a centralized database such as InterSystems IRIS, enabling distributed hyperparameter tuning across multiple machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optuna's native Concurrency + MLflow model registry
&lt;/h2&gt;

&lt;p&gt;We can combine Optuna for hyperparameter tuning and MLflow for experiment tracking and model registry. This way, we can leverage the same MLflow model registry capabilities shown in this &lt;a href="https://github.com/JorgeIvanJH/IRIS_and_MLflow-Continuous-Training-Pipeline" rel="noopener noreferrer"&gt;repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One of the main advantages of Optuna is how easy it is to scale hyperparameter tuning across processes or even across machines. We can run the same optimization study from different machines, and as long as all of them point to the same storage database, all workers will contribute trials to the same study. As trials finish, Optuna can use the accumulated results to guide future samples.&lt;/p&gt;

&lt;p&gt;In the example below, we run multiple workers against the same Optuna study. Running this as a separate Python script, not in a standard Jupyter notebook, allows parallel hyperparameter tuning with MLflow tracking. MLflow keeps track of the parent run, each child trial run, the final best parameters, the best cross-validation score, and the final trained model.&lt;/p&gt;

&lt;p&gt;The cell below ran 3200 trials in 25 minutes on a Windows laptop with 16 cores, using 16 workers with 200 trials each. Each trial used 3 cross-validation splits.&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;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lightgbm&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mlflow&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mlflow.lightgbm&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mlflow.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;infer_signature&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KFold&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fetch_california_housing&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;

&lt;span class="n"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;STORAGE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite:///optuna_lgbm.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# for local testing
&lt;/span&gt;

&lt;span class="c1"&gt;# Hyperparameter tuning configuration
&lt;/span&gt;&lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpu_count&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;NUM_TRIALS_PER_WORKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="n"&gt;BASE_SEED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="n"&gt;NUM_CV_SPLITS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="c1"&gt;# 5 or 10 would be better
&lt;/span&gt;&lt;span class="n"&gt;EXPERIMENT_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LightGBM Hyperparameter Tuning with Optuna and MLflow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;crossvalstrategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KFold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_splits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NUM_CV_SPLITS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shuffle&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;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Load dataset
&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_california_housing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;return_X_y&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;as_frame&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;X&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;col&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;median_house_value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&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;learning_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;learning_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&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="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n_estimators&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&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_estimators&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_leaves&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_categorical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_leaves&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="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&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="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_categorical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_bin&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="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;random_state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verbosity&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="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;n_jobs&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;parent_run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MLFLOW_PARENT_RUN_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;run_name&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;trial_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&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;nested&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;parent_run_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;parent_run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# tags={"mlflow.parentRunId": parent_run_id} if parent_run_id else None,
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;child_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LGBMRegressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;crossvalstrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;scoring&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;neg_mean_squared_error&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_jobs&lt;/span&gt;&lt;span class="o"&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="n"&gt;crossval_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;scores&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;# Log current trial's error metric
&lt;/span&gt;        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_metrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cv_mse_mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;crossval_score&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;fold_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_metric&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;fold_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fold_idx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_mse&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;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Make it easy to retrieve the best-performing child run later
&lt;/span&gt;        &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_user_attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child_run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&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;crossval_score&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;worker_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;
    &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tracking_uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MLFLOW_TRACKING_URI&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXPERIMENT_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MLFLOW_PARENT_RUN_ID&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;parent_run_id&lt;/span&gt;

    &lt;span class="n"&gt;study&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_study&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STORAGE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sampler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;samplers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TPESampler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;worker_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;optimize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;n_trials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NUM_TRIALS_PER_WORKER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;show_progress_bar&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="n"&gt;n_jobs&lt;/span&gt;&lt;span class="o"&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;worker_id&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="c1"&gt;# MLflow setup
&lt;/span&gt;    &lt;span class="n"&gt;datetime_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;RUN_NAME&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;parent_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime_str&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;STUDY_NAME&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;optuna_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime_str&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;tracking_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MLFLOW_TRACKING_URI&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tracking_uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tracking_uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXPERIMENT_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;experiment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_experiment_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXPERIMENT_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;experiment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;experiment_id&lt;/span&gt;


    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;RUN_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log_system_metrics&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;parent_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parent_run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parent_run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MLFLOW_PARENT_RUN_ID&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;parent_run_id&lt;/span&gt;

        &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_study&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minimize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STUDY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STORAGE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;load_if_exists&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="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_params&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_trials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NUM_TRIALS_PER_WORKER&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_workers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cv_n_splits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;crossvalstrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_splits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;study_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;STUDY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="n"&gt;worker_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;STUDY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_run_id&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;worker_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;worker_args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;study&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_study&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STUDY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STORAGE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;best_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;
        &lt;span class="n"&gt;best_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_value&lt;/span&gt;
        &lt;span class="n"&gt;best_child_run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_attrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_params&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;best_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&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;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;best_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;
        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;best_cv_mse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best_value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;best_child_run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;best_child_run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_child_run_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Train final model on full dataset with best hyperparameters. Important: keep same seed
&lt;/span&gt;        &lt;span class="n"&gt;final_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LGBMRegressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;best_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;verbosity&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;n_jobs&lt;/span&gt;&lt;span class="o"&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="n"&gt;final_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;input_sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;infer_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_sample&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;final_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_sample&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lightgbm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;lgb_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;final_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;best_model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;input_example&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;X&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;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code above works as a proof of concept when working across different machines. Each machine or process can point to the same shared Optuna storage database and contribute trials to the same study.&lt;/p&gt;

&lt;p&gt;However, if we are using a single PC, the simpler version below is usually preferable. It runs the same study with parallel jobs controlled by Optuna's n_jobs parameter. This approach is simpler and can achieve similar performance, although the exact trials and final best model are not guaranteed to be identical to the multiprocessing version.&lt;/p&gt;

&lt;p&gt;The code below also ran 3200 trials, in this case in 27 minutes.&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;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lightgbm&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mlflow&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mlflow.lightgbm&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mlflow.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;infer_signature&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KFold&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fetch_california_housing&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;

&lt;span class="n"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;STORAGE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite:///optuna_lgbm.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# for local testing
&lt;/span&gt;

&lt;span class="c1"&gt;# Hyperparameter tuning configuration
&lt;/span&gt;&lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpu_count&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;NUM_TRIALS_PER_WORKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="n"&gt;BASE_SEED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="n"&gt;NUM_CV_SPLITS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="c1"&gt;# 5 or 10 would be better
&lt;/span&gt;&lt;span class="n"&gt;EXPERIMENT_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LightGBM Hyperparameter Tuning with Optuna and MLflow 2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;crossvalstrategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KFold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_splits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NUM_CV_SPLITS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shuffle&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;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Load dataset
&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_california_housing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;return_X_y&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;as_frame&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;X&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;col&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;median_house_value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&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;learning_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;learning_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&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="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n_estimators&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&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_estimators&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_leaves&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_categorical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_leaves&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="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&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="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_categorical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_bin&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="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;random_state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verbosity&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="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;n_jobs&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;parent_run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MLFLOW_PARENT_RUN_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;run_name&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;trial_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&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;nested&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;parent_run_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;parent_run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# tags={"mlflow.parentRunId": parent_run_id} if parent_run_id else None,
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;child_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LGBMRegressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;crossvalstrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;scoring&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;neg_mean_squared_error&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_jobs&lt;/span&gt;&lt;span class="o"&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="n"&gt;crossval_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;scores&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;# Log current trial's error metric
&lt;/span&gt;        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_metrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cv_mse_mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;crossval_score&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;fold_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_metric&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;fold_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fold_idx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_mse&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;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Make it easy to retrieve the best-performing child run later
&lt;/span&gt;        &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_user_attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child_run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&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;crossval_score&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="c1"&gt;# MLflow setup
&lt;/span&gt;    &lt;span class="n"&gt;datetime_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;RUN_NAME&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;parent_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime_str&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;STUDY_NAME&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;optuna_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime_str&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;tracking_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MLFLOW_TRACKING_URI&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tracking_uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tracking_uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXPERIMENT_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;experiment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_experiment_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXPERIMENT_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;experiment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;experiment_id&lt;/span&gt;


    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;RUN_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log_system_metrics&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;parent_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parent_run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parent_run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MLFLOW_PARENT_RUN_ID&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;parent_run_id&lt;/span&gt;

        &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_study&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minimize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STUDY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STORAGE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;load_if_exists&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="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_params&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_trials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NUM_TRIALS_PER_WORKER&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_workers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cv_n_splits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;crossvalstrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_splits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;study_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;STUDY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="n"&gt;study&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_study&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STUDY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STORAGE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;optimize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;n_trials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NUM_TRIALS_PER_WORKER&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;show_progress_bar&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="n"&gt;n_jobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;best_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;
        &lt;span class="n"&gt;best_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_value&lt;/span&gt;
        &lt;span class="n"&gt;best_child_run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_attrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_params&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;best_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&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;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;best_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;
        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;best_cv_mse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best_value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;best_child_run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;best_child_run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_child_run_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Train final model on full dataset with best hyperparameters. Important: keep same seed
&lt;/span&gt;        &lt;span class="n"&gt;final_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LGBMRegressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;best_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;verbosity&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;n_jobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;final_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;input_sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;infer_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_sample&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;final_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_sample&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lightgbm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;lgb_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;final_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;best_model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;input_example&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;X&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;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a result of running either script, we get a parent run in MLflow with the final best model trained using the best hyperparameters found across the 3200 trials. The parent run also stores the best hyperparameters, the best cross-validation score, and the ID of the best child run. Each child run contains the parameters and metrics for one Optuna trial.&lt;/p&gt;

&lt;p&gt;All of this can be explored in the MLflow UI, for example at &lt;a href="http://localhost:5000/#/experiments" rel="noopener noreferrer"&gt;http://localhost:5000/#/experiments&lt;/a&gt;, where we can inspect the parent run, compare child runs, and download or register the final model.&lt;/p&gt;

&lt;p&gt;In the image below, we see two plots from MLflow's UI. On the left, we get a sense of the search space by comparing the mean cross-validation MSE across trials with different values of max_depth and num_leaves. On the right, we see the 100 worst models, meaning the trials with the highest mean squared error across cross-validation. The best found model achieved a score of approximately 0.199580.&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.amazonaws.com%2Fuploads%2Farticles%2Fofddwuf9p4n8tu7100m8.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.amazonaws.com%2Fuploads%2Farticles%2Fofddwuf9p4n8tu7100m8.png" alt=" " width="799" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Optuna Concurrency + IRIS DB
&lt;/h2&gt;

&lt;p&gt;When trying to replicate the same process with IRIS DB as the Optuna storage backend, multiple issues arose when running more than 4 workers in parallel. This is likely related to how each worker process creates its own connection to IRIS and writes trial metadata concurrently to the same Optuna study.&lt;/p&gt;

&lt;p&gt;The code below worked fine with up to 3 workers running at the same time. Another option is to keep a single Python process pointing to IRIS and set Optuna's n_jobs parameter to the number of concurrent jobs we want (just as we did above). This approach uses threads inside one process, which can be simpler from a database-connection perspective because it avoids multiple independent Python processes creating separate connections to IRIS.&lt;/p&gt;

&lt;p&gt;However, this approach is not always equivalent to multiprocessing. Since Optuna's n_jobs uses threads, CPU-bound Python code can be limited by Python's GIL. In this specific example, most of the expensive work is done by LightGBM and scikit-learn routines, so threading may still provide useful speedup, but it may not scale the same way as true multiprocessing.&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;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lightgbm&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.pool&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NullPool&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KFold&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fetch_california_housing&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;

&lt;span class="n"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpu_count&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGEABLE
&lt;/span&gt;&lt;span class="n"&gt;NUM_TRIALS_PER_WORKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGEABLE
&lt;/span&gt;&lt;span class="n"&gt;STUDY_NAME&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;IRIS_lightgbm_study_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d_%H-%M-%S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGEABLE
&lt;/span&gt;&lt;span class="n"&gt;BASE_SEED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGEABLE
&lt;/span&gt;
&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IRIS_SERVER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IRIS_PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IRIS_NAMESPACE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IRIS_USERNAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IRIS_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;STORAGE_URL&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;iris://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;crossvalstrategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KFold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_splits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shuffle&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;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Load Dataset
&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_california_housing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;return_X_y&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;as_frame&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;X&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;col&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;median_house_value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="o"&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;learning_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;learning_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&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="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n_estimators&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&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_estimators&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_leaves&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_categorical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_leaves&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="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&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="c1"&gt;# CHANGEABLE
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_categorical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_bin&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="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;random_state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BASE_SEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verbosity&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="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;n_jobs&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LGBMRegressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;crossvalstrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;scoring&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;neg_mean_squared_error&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_jobs&lt;/span&gt;&lt;span class="o"&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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;scores&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="n"&gt;worker_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;
    &lt;span class="n"&gt;worker_storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_storage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;study&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_study&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker_storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sampler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;samplers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TPESampler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BASE_SEED&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;worker_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;optimize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_trials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NUM_TRIALS_PER_WORKER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;show_progress_bar&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="n"&gt;n_jobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&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;worker_id&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_storage&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;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RDBStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STORAGE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;engine_kwargs&lt;/span&gt;&lt;span class="o"&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;poolclass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NullPool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connect_args&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;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;# Helps with heavy concurrent writes
&lt;/span&gt;        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="n"&gt;main_storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_storage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_study&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minimize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STUDY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;main_storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;load_if_exists&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="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main_storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_engine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;main_storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_engine&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;worker_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;worker_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;STUDY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="bp"&gt;None&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;worker_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NUM_WORKERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;worker_args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;final_storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_storage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;final_study&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_study&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;study_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STUDY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;final_storage&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Overall Best Value: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;final_study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Overall Best Params: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;final_study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_params&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;Optuna saves the study metadata in IRIS for future reference. This includes studies, trials, trial parameters, trial values, intermediate values, and related metadata in the Optuna storage tables created in IRIS.&lt;/p&gt;

&lt;p&gt;For further performance analysis, we can query these tables directly or, preferably, load the study back through Optuna and use Optuna's built-in visualization and analysis tools to inspect the optimization history, parameter importance, and trial performance.&lt;/p&gt;

&lt;p&gt;The image below shows the Optuna storage tables created in IRIS DB.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&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.amazonaws.com%2Fuploads%2Farticles%2Fsvx8slxzjpp0qc5kttjf.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.amazonaws.com%2Fuploads%2Farticles%2Fsvx8slxzjpp0qc5kttjf.png" alt=" " width="715" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Discovering PII Inside InterSystems IRIS</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 16 Jun 2026 15:34:39 +0000</pubDate>
      <link>https://dev.to/intersystems/discovering-pii-inside-intersystems-iris-1i2l</link>
      <guid>https://dev.to/intersystems/discovering-pii-inside-intersystems-iris-1i2l</guid>
      <description>&lt;p&gt;Data privacy regulations such as GDPR, LGPD, and HIPAA demand that organizations know exactly where Personally Identifiable Information (PII) lives inside their databases. Yet in practice, most teams rely on manual inventories, tribal knowledge, or external scanning tools that require data to leave the database engine — a process that itself creates privacy and security risks.&lt;/p&gt;

&lt;p&gt;This article presents an MVP that takes a different approach: it runs PII detection &lt;strong&gt;inside&lt;/strong&gt; InterSystems IRIS using Embedded Python, analyzing data where it lives and never exporting it to an external process. The result is a lightweight, non-intrusive utility that scans your tables, identifies PII using AI, and produces a structured CSV report — all without data ever leaving the IRIS process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: PII You Don't Know You Have
&lt;/h2&gt;

&lt;p&gt;Organizations today face a painful blind spot. A typical IRIS instance may contain hundreds of tables across dozens of schemas, some holding decades of accumulated data. Columns named &lt;code&gt;ContactInfo&lt;/code&gt;, &lt;code&gt;Notes&lt;/code&gt;, or &lt;code&gt;Description&lt;/code&gt; might silently contain social security numbers, email addresses, or government IDs — sometimes intentionally, sometimes as a side effect of free-text fields that capture whatever users type in.&lt;/p&gt;

&lt;p&gt;Traditional approaches to PII discovery share a common flaw: they require data extraction. You export samples, send them to an external service, or pipe them through a standalone tool. Every step in that pipeline is an additional attack surface and a potential compliance violation.&lt;/p&gt;

&lt;p&gt;The principle of &lt;strong&gt;data sovereignty&lt;/strong&gt; — keeping data within its jurisdiction and under controlled access — suggests a better path: bring the analysis to the data, not the data to the analysis.&lt;/p&gt;

&lt;p&gt;This is not just a technical preference; it is a governance requirement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GDPR (EU)&lt;/strong&gt; — Article 28 requires that any processing of personal data by a third-party processor be governed by a binding contract covering subject-matter, duration, purpose, data types, and obligations [&lt;a href="https://gdpr.eu/article-28-processor/" rel="noopener noreferrer"&gt;Art. 28 GDPR&lt;/a&gt;]. Article 44 extends this further: any transfer of personal data to a third country is permitted only if the conditions of Chapter V are met, ensuring the level of protection guaranteed by the Regulation is not undermined [&lt;a href="https://gdpr.eu/article-44-transfer-of-personal-data/" rel="noopener noreferrer"&gt;Art. 44 GDPR&lt;/a&gt;]. Every external tool you send data to becomes a new processor — and every cross-border transfer triggers these obligations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LGPD (Brazil)&lt;/strong&gt; — Brazil's Lei Geral de Proteção de Dados mirrors GDPR's principles. Article 5(XV) defines "data processing" broadly to include any operation with personal data, and Article 37 requires the appointment of a Data Protection Officer (DPO) by controllers [&lt;a href="https://www.planalto.gov.br/ccivil_03/_ato2015-2018/2018/lei/l13709.htm" rel="noopener noreferrer"&gt;Lei nº 13.709/2018&lt;/a&gt;]. Any external PII scanning service would itself be classified as a processor under the law.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HIPAA (US)&lt;/strong&gt; — The Security Rule mandates that covered entities and business associates implement technical safeguards to protect the confidentiality, integrity, and availability of electronic protected health information (ePHI). Specifically, the Transmission Security standard (45 CFR §164.312(e)) requires technical security measures to guard against unauthorized access to ePHI that is being transmitted over an electronic network [&lt;a href="https://www.hhs.gov/hipaa/for-professionals/security/laws-regulations/index.html" rel="noopener noreferrer"&gt;HIPAA Security Rule Summary&lt;/a&gt;]. Every time ePHI leaves the database engine for an external scan, this safeguard is put at risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running the scan inside the database engine eliminates the transmission step entirely, simplifying compliance and reducing risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: Three Decoupled Components
&lt;/h2&gt;

&lt;p&gt;The utility follows a simple but deliberate separation of concerns. Three independent components cooperate in a pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PIIScanner  →  PIIIdentifier  →  PIIReporter
(database)     (AI detection)     (reporting)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PIIIdentifier&lt;/strong&gt; — Wraps the AI detection library. It has zero knowledge of IRIS, SQL, or database schemas. Its single method, &lt;code&gt;identify(text)&lt;/code&gt;, takes a string and returns the highest-confidence PII entity type (e.g., &lt;code&gt;"EMAIL_ADDRESS"&lt;/code&gt;, &lt;code&gt;"PERSON"&lt;/code&gt;, &lt;code&gt;"CPF"&lt;/code&gt;) or &lt;code&gt;None&lt;/code&gt;. This isolation means the detection logic can be tested, swapped, or upgraded without touching the database layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PIIScanner&lt;/strong&gt; — The only component that interacts with IRIS. It queries &lt;code&gt;INFORMATION_SCHEMA.TABLES&lt;/code&gt; to discover user tables, samples up to N rows per table via &lt;code&gt;SELECT TOP N *&lt;/code&gt;, feeds each column's values to the identifier, and collects findings. It respects schema exclusion patterns (exact match and wildcard prefix like &lt;code&gt;"Ens*"&lt;/code&gt;) and lets the caller configure the sample size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PIIReporter&lt;/strong&gt; — Deduplicates findings and writes a CSV with five columns: &lt;code&gt;schema_name, table_name, column_name, pii_type, confidence&lt;/code&gt;. The confidence score (0.0–1.0) helps reviewers prioritize findings and identify likely false positives.&lt;/p&gt;

&lt;p&gt;This separation is not accidental. It means the identifier could be replaced with a more powerful model tomorrow without changing a single line of scanner or reporter code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft Presidio and spaCy: The Detection Engine
&lt;/h2&gt;

&lt;p&gt;The PIIIdentifier is powered by &lt;a href="https://microsoft.github.io/presidio/" rel="noopener noreferrer"&gt;Microsoft Presidio&lt;/a&gt;, an open-source data protection and de-identification framework. Presidio is the current detection engine, but the architecture is deliberately engine-agnostic — the &lt;code&gt;PIIIdentifier&lt;/code&gt; wrapper fully isolates the detection library from the scanner and reporter. Swapping to a different detection approach would only require changes to that one module, leaving the rest of the pipeline untouched. Presidio's analyzer combines two detection strategies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pattern-based recognizers&lt;/strong&gt; — Regular expressions and checksum validators for structured identifiers: email addresses, phone numbers, SSNs, credit card numbers, CPF, and dozens more. These recognizers are deterministic and language-agnostic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NLP-based recognizers&lt;/strong&gt; — Machine learning models that detect entity types like PERSON, LOCATION, and ORGANIZATION from natural language context. This is where spaCy comes in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The utility configures Presidio with two spaCy models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;en_core_web_sm&lt;/code&gt; — English small model (~12 MB)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pt_core_news_sm&lt;/code&gt; — Portuguese small model (~13 MB)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each row of data is analyzed against both languages, and the highest-confidence result wins. Multi-language support is essential for this kind of tool to be useful for users around the world — databases rarely contain data in a single language, and PII detection that only understands English would miss critical findings in Portuguese, Spanish, German, or any other language. The current MVP supports English and Portuguese as a starting point, but the architecture makes it straightforward to add more spaCy models for additional languages.&lt;/p&gt;

&lt;p&gt;For every text input, the &lt;code&gt;identify()&lt;/code&gt; method iterates through both language analyzers, collects all results, and returns the entity type with the highest confidence score:&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;identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;best_entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;best_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_analyzer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lang&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;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;best_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;
                &lt;span class="n"&gt;best_entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_type&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;best_entity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design means a Brazilian CPF mentioned in an English sentence will still be caught by the PT analyzer's pattern recognizer, even though the surrounding text is English.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Inside IRIS: The Embedded Python Advantage
&lt;/h2&gt;

&lt;p&gt;The entire utility runs as a Python module inside the IRIS process via &lt;code&gt;irispython&lt;/code&gt;. No external API calls, no data exports, no network transfers. The scanner uses &lt;code&gt;iris.sql.exec()&lt;/code&gt; — IRIS's native Python SQL interface — to query metadata and sample data directly within the engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;irispython &lt;span class="nt"&gt;-m&lt;/span&gt; irisapp.pii_discovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single command starts the scan. The output is a CSV file written to the mounted volume, immediately available on the host machine.&lt;/p&gt;

&lt;p&gt;The utility also integrates with IRIS's built-in Task Scheduler. A &lt;code&gt;%SYS.Task.Definition&lt;/code&gt; subclass (&lt;code&gt;PIIScannerTask&lt;/code&gt;) exposes configurable &lt;code&gt;OutputPath&lt;/code&gt; and &lt;code&gt;SampleSize&lt;/code&gt; properties in the Admin Portal, and its &lt;code&gt;OnTask()&lt;/code&gt; method invokes the Python module via &lt;code&gt;%SYS.Python.Import()&lt;/code&gt;. The task is registered automatically during Docker build and can be scheduled to run periodically — for instance, a weekly PII inventory scan that appends results to a central compliance report.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One-shot scan from the command line&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;iris irispython &lt;span class="nt"&gt;-m&lt;/span&gt; irisapp.pii_discovery

&lt;span class="c"&gt;# Scan with custom namespace and sample size&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;iris irispython &lt;span class="nt"&gt;-m&lt;/span&gt; irisapp.pii_discovery &lt;span class="nt"&gt;-n&lt;/span&gt; USER &lt;span class="nt"&gt;-s&lt;/span&gt; 50

&lt;span class="c"&gt;# Populate sample data + scan in one command&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;iris irispython &lt;span class="nt"&gt;-m&lt;/span&gt; irisapp.pii_discovery &lt;span class="nt"&gt;--populate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Sample Database: Testing with Realistic Data
&lt;/h2&gt;

&lt;p&gt;To make the utility immediately testable, the project includes a sample database in the &lt;code&gt;PIISample&lt;/code&gt; schema with three tables that cover the main PII patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PIISample.Patients&lt;/strong&gt; — Structured single-field PII. Each column holds one type of personal data: full names, email addresses, phone numbers, SSNs/CPFs, and street addresses. The table deliberately mixes US and Brazilian records to exercise both NLP models. Non-PII columns (Diagnosis, AdmissionDate) serve as internal controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PIISample.CustomerFeedback&lt;/strong&gt; — Free-text PII. Narrative paragraphs contain PII embedded in natural language — the hardest detection pattern. Examples include &lt;em&gt;"My SSN is 111-22-3333 for insurance verification"&lt;/em&gt; and &lt;em&gt;"Meu CPF é 345.678.901-22"&lt;/em&gt;. Two rows contain no PII at all, acting as negative controls within the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PIISample.Products&lt;/strong&gt; — No PII. A control table with product names, categories, prices, and stock quantities. Ideally the scanner should produce zero findings here — in practice, the small NLP model produces false positives, which we will examine in the results section.&lt;/p&gt;

&lt;p&gt;The sample data is populated by a Python function (&lt;code&gt;populate()&lt;/code&gt;) that runs during Docker build and can be re-invoked at any time. It uses &lt;code&gt;DROP TABLE IF EXISTS&lt;/code&gt; before each &lt;code&gt;CREATE TABLE&lt;/code&gt;, making it idempotent and safe to call repeatedly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results: What the Scanner Found — and What It Got Wrong
&lt;/h2&gt;

&lt;p&gt;Running the scanner against the sample database produces something like the following report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;schema_name,table_name,column_name,pii_type,confidence
PIISample,CustomerFeedback,CustomerName,PERSON,0.85
PIISample,CustomerFeedback,FeedbackText,EMAIL_ADDRESS,1.0
PIISample,CustomerFeedback,CreatedAt,DATE_TIME,0.85
PIISample,Patients,FullName,PERSON,0.85
PIISample,Patients,Email,EMAIL_ADDRESS,1.0
PIISample,Patients,Phone,PHONE_NUMBER,0.4
PIISample,Patients,SSN,PHONE_NUMBER,0.4
PIISample,Patients,DateOfBirth,DATE_TIME,0.85
PIISample,Patients,Address,LOCATION,0.85
PIISample,Patients,Diagnosis,LOCATION,0.85
PIISample,Patients,AdmissionDate,DATE_TIME,0.85
PIISample,Products,ProductName,PERSON,0.85
PIISample,Products,Category,LOCATION,0.85
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The true positives are clear: names detected as PERSON, emails as EMAIL_ADDRESS, phone numbers as PHONE_NUMBER, addresses as LOCATION. Confidence scores help reviewers prioritize — well-structured PII like emails consistently scores 0.85, while borderline cases like false positives on the Products table score below 0.5.&lt;/p&gt;

&lt;p&gt;But the results also reveal the limitations of the current approach — and they are not limited to edge cases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Products — not a clean pass.&lt;/strong&gt; The Products table was designed as a no-PII control, containing only product names, categories, prices, and stock quantities. Yet the scanner reports &lt;code&gt;PERSON&lt;/code&gt; in ProductName and &lt;code&gt;LOCATION&lt;/code&gt; in Category. Product names like "Wireless Mouse" and categories like "Sports" are misidentified by the NLP model because the small spaCy model lacks the contextual understanding to distinguish generic nouns from personal names or place names. This is the most striking false positive in the results: a table with zero PII produces two findings, demonstrating exactly where the small model trade-off hurts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagnosis flagged as LOCATION.&lt;/strong&gt; Medical diagnoses like "Hypertension" and "Diabetes Type 2" are misclassified as LOCATION. This is another NLP false positive — the small model confuses medical terminology with geographic references.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSN detected as PHONE_NUMBER.&lt;/strong&gt; The Patients.SSN column contains values like &lt;code&gt;123-45-6789&lt;/code&gt; (US SSN) and &lt;code&gt;123.456.789-00&lt;/code&gt; (Brazilian CPF). Presidio has dedicated recognizers for both &lt;code&gt;US_SSN&lt;/code&gt; and &lt;code&gt;CPF&lt;/code&gt;, but the small spaCy models sometimes assign a higher confidence score to the PHONE_NUMBER recognizer for these digit-heavy patterns. The scanner reports the highest-scoring entity — which in this case is the wrong one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Date columns flagged as DATE_TIME.&lt;/strong&gt; Values like &lt;code&gt;1985-03-15&lt;/code&gt; trigger the DATE_TIME recognizer. Whether dates of birth and admission dates constitute PII is context-dependent: under HIPAA they are, under some interpretations of GDPR they might not be (on their own). The scanner makes no policy judgment — it reports what it finds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One PII type per column.&lt;/strong&gt; The scanner's &lt;code&gt;scan_column()&lt;/code&gt; method returns the first PII type found in a column. If a column contains both email addresses and phone numbers (as FeedbackText does), only the first type detected gets reported. This is by design for the MVP — a full inventory might list all detected types per column.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spaCy Small Model Trade-off
&lt;/h2&gt;

&lt;p&gt;The false positives and misclassifications stem from a deliberate architectural choice: using spaCy's &lt;strong&gt;small&lt;/strong&gt; models (&lt;code&gt;_sm&lt;/code&gt; suffix) rather than medium (&lt;code&gt;_md&lt;/code&gt;) or large (&lt;code&gt;_lg&lt;/code&gt;) variants.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Size (EN)&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;th&gt;Load Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;en_core_web_sm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~12 MB&lt;/td&gt;
&lt;td&gt;Lower&lt;/td&gt;
&lt;td&gt;~100 MB&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;en_core_web_md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~40 MB&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;td&gt;~300 MB&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;en_core_web_lg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~560 MB&lt;/td&gt;
&lt;td&gt;Highest&lt;/td&gt;
&lt;td&gt;~1 GB&lt;/td&gt;
&lt;td&gt;Slow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The small models were chosen for the MVP because they keep the Docker image lean, startup fast, and run comfortably within the memory constraints of a containerized IRIS instance. For a proof-of-concept that needs to demonstrate feasibility, this is the right trade-off.&lt;/p&gt;

&lt;p&gt;But the trade-off is real. Small models have less training data, fewer word vectors, and coarser entity boundaries. In practice, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More false positives&lt;/strong&gt; — The sample database results demonstrate this concretely: the Products table, which contains zero PII, produces two false positive findings (&lt;code&gt;PERSON&lt;/code&gt; in ProductName and &lt;code&gt;LOCATION&lt;/code&gt; in Category). Common nouns like "Wireless Mouse" or "Sports" are misidentified because the small model lacks the word vectors to distinguish them from personal names or place names. Similarly, medical diagnoses like "Hypertension" are misclassified as LOCATION.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More misclassifications&lt;/strong&gt; — SSN and CPF patterns, while matched by Presidio's regex recognizers, can be out-scored by the NLP-based PHONE_NUMBER recognizer when the model's confidence calibration is off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poorer context understanding&lt;/strong&gt; — The small model may fail to distinguish &lt;em&gt;"My name is John"&lt;/em&gt; (PERSON) from &lt;em&gt;"John Deere Equipment"&lt;/em&gt; (ORGANIZATION) without sufficient surrounding context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upgrading to medium or large models would improve accuracy significantly, but at a cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; — The large English model alone requires ~1 GB of RAM at runtime, plus a similar footprint for Portuguese. In a containerized environment, this constrains how many workloads can run alongside IRIS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; — Loading large models adds 5–10 seconds of startup time per scan. For a scheduled task running at 2 AM, this is acceptable. For an interactive scan triggered from a UI, it may not be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image size&lt;/strong&gt; — The Docker image would grow by hundreds of megabytes, increasing build times and storage requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An alternative path is replacing spaCy with transformer-based models (e.g., HuggingFace BERT or RoBERTa fine-tuned for NER), which offer state-of-the-art accuracy. Presidio supports this via its &lt;code&gt;NlpEngineProvider&lt;/code&gt; — you can configure a Transformers-backed engine instead of spaCy. But transformer models carry even heavier resource requirements: GPU inference for acceptable latency, multiple gigabytes of memory, and significantly longer processing times per text.&lt;/p&gt;

&lt;p&gt;The architecture of this MVP — with the PIIIdentifier fully isolated from the scanner — makes this upgrade path straightforward. Swap the NLP engine configuration, and the rest of the pipeline continues to work unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros and Cons
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Strengths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data sovereignty.&lt;/strong&gt; Data never leaves the IRIS process. No external APIs, no network transfers, no intermediate files containing raw PII. The analysis happens where the data lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-friction deployment.&lt;/strong&gt; Runs inside the same Docker container as IRIS. No separate service to deploy, monitor, or secure. One command to scan, one CSV file as output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bilingual detection.&lt;/strong&gt; Dual-language support (English + Portuguese) out of the box, with a clean pattern for adding more languages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-intrusive.&lt;/strong&gt; Uses sampling (&lt;code&gt;SELECT TOP N&lt;/code&gt;) rather than full table scans. Configurable sample size and schema exclusions let you control scope and impact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task Scheduler integration.&lt;/strong&gt; Automatic periodic scans via the IRIS Admin Portal, with configurable output path and sample size — no cron jobs or external schedulers needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modular architecture.&lt;/strong&gt; AI detection, database scanning, and reporting are fully decoupled. Upgrading the detection engine is a one-file change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small model accuracy.&lt;/strong&gt; As discussed, the spaCy small models produce false positives and misclassifications. This is the most significant limitation for production use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One PII type per column.&lt;/strong&gt; The current scanner reports only the highest-confidence entity type per column, not the full set of PII types present. A column containing both emails and phone numbers will only report one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No column-level exclusion.&lt;/strong&gt; You can exclude schemas, but not individual columns. A &lt;code&gt;notes&lt;/code&gt; column that is known to contain PII might be intentionally excluded from the report to avoid noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No incremental scanning.&lt;/strong&gt; Every run scans all tables from scratch. There is no tracking of previously scanned tables or columns, which limits scalability for large databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample-based detection.&lt;/strong&gt; If PII exists only in row 101 and beyond, a &lt;code&gt;SELECT TOP 100&lt;/code&gt; sample will miss it. Random sampling (e.g., &lt;code&gt;TABLESAMPLE&lt;/code&gt;) would be more robust but is not yet implemented.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No false negative analysis.&lt;/strong&gt; No systematic search for false negatives was performed in this work. PII that exists in the database but is not flagged by the scanner goes unnoticed — unlike false positives, which are visible in the report and can be reviewed by a human, false negatives are invisible. The report should be treated as a lower bound of PII presence, not a complete inventory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker build time.&lt;/strong&gt; Installing Presidio, spaCy, and downloading two NLP models adds significant time to the Docker build. This is a one-time cost but can be painful during development iterations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;The project runs on InterSystems IRIS Community Edition in Docker. Clone the repository, build the image, and start the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose build
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sample database is populated automatically during the build. To run your first scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;iris irispython &lt;span class="nt"&gt;-m&lt;/span&gt; irisapp.pii_discovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The report will be written to &lt;code&gt;pii_report.csv&lt;/code&gt; in the project root. Open it, review the findings, and compare them against the sample data to understand what the scanner catches — and what it doesn't.&lt;/p&gt;

&lt;p&gt;You can check the sample database &lt;a href="http://localhost:55038/csp/sys/exp/%25CSP.UI.Portal.SQL.Home.zen?$NAMESPACE=IRISAPP" rel="noopener noreferrer"&gt;here&lt;/a&gt;, then choosing the &lt;code&gt;PIISample&lt;/code&gt; schema. Use default IRIS Community Version credentials (_system/SYS).&lt;/p&gt;

&lt;p&gt;From there, try the &lt;code&gt;--populate&lt;/code&gt; flag to reset the sample data, change the sample size with &lt;code&gt;-s&lt;/code&gt;, or point the scanner at a different namespace with &lt;code&gt;-n&lt;/code&gt;. The &lt;code&gt;--populate&lt;/code&gt; flag is particularly useful: it resets the sample tables and runs the scan in one step, making iteration fast.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is an MVP — a proof of concept that demonstrates the compute-to-data approach for PII discovery inside InterSystems IRIS. The small NLP models are a starting point, not a ceiling. The architecture is built to grow.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was developed with the assistance of Artificial Intelligence tools for drafting and language refinement. All technical validation and final review were performed by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>security</category>
      <category>python</category>
      <category>database</category>
    </item>
    <item>
      <title>AI-Powered Clinical Matching: Introducing iris-medmatch</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sat, 30 May 2026 18:50:37 +0000</pubDate>
      <link>https://dev.to/intersystems/ai-powered-clinical-matching-introducing-iris-medmatch-21a9</link>
      <guid>https://dev.to/intersystems/ai-powered-clinical-matching-introducing-iris-medmatch-21a9</guid>
      <description>&lt;p&gt;In the modern healthcare landscape, finding clinically similar patients often feels like looking for a needle in a haystack. Traditional keyword searches often fail because medical language is highly nuanced; a search for "Heart Failure" might miss a record containing "Congestive Cardiac Failure."&lt;/p&gt;
&lt;p&gt;I am excited to share&lt;strong&gt; iris-medmatch&lt;/strong&gt;, an AI-powered patient matching engine built on &lt;em&gt;&lt;strong&gt;InterSystems IRIS for Health&lt;/strong&gt;&lt;/em&gt;. By leveraging &lt;em&gt;Vector Search,&lt;/em&gt; this tool understands clinical intent rather than just matching literal strings.&lt;br&gt;## The Core Innovation: Semantic Clinical Search&lt;/p&gt;
&lt;p&gt;`iris-medmatch` bridges the gap between raw FHIR data and actionable AI insights. By utilizing the `all-MiniLM-L6-v2` model, the engine transforms clinical conditions into mathematical vectors.&lt;/p&gt;
&lt;p&gt;While standard searches look for exact words, this engine understands **clinical context**. For example, it can match a patient with "Hypertension" to a search for "High Blood Pressure" using mathematical vector similarity.&lt;/p&gt;
&lt;h4&gt;✨ Key Technical Features&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core&lt;/strong&gt;: InterSystems IRIS , Embedded Python, InterSystems FHIR Server, Vector search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: Python, ONNX Runtime, HuggingFace Transformers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Angular 18+&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Technical Architecture&lt;/h4&gt;
&lt;p&gt;The strength of this solution lies in its architectural efficiency. By running Transformers via Embedded Python, we eliminate "data gravity" issues. The data stays in IRIS, and the AI processing happens where the data lives.&lt;/p&gt;
&lt;p&gt;🚀 Application Walkthrough&lt;/p&gt;
&lt;p&gt;1. Semantic Similarity Search (The "Wow" Factor)&lt;/p&gt;
&lt;p&gt;This module uses Vector Search to understand medical synonyms. A search for "Cardiac Issues" will mathematically find "Myocardial Infarction" by comparing their vector positions within IRIS. This is achieved using Native IRIS SQL to calculate similarity scores in sub-seconds.&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.amazonaws.com%2Fuploads%2Farticles%2Fz49enk42f9179fu48kz7.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.amazonaws.com%2Fuploads%2Farticles%2Fz49enk42f9179fu48kz7.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2. Patient Directory &amp;amp; Condition Enrichment&lt;/p&gt;
&lt;p&gt;This module manages existing FHIR resources. Users can add new diagnoses through a high-performance modal, demonstrating real-time synchronization between standard FHIR data and AI-ready vector data.&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.amazonaws.com%2Fuploads%2Farticles%2F9zt9snzhosh35dnkgdgr.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.amazonaws.com%2Fuploads%2Farticles%2F9zt9snzhosh35dnkgdgr.png" alt=" " width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3. New Patient Registration&lt;/p&gt;
&lt;p&gt;A streamlined entry point for creating new `Patient` resources within the InterSystems ecosystem. This features direct interaction with the FHIR R4 Repository via standard RESTful POST requests, ensuring data is indexed and searchable immediately.&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.amazonaws.com%2Fuploads%2Farticles%2Fwq9vlgx4gygeoskxk1eb.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.amazonaws.com%2Fuploads%2Farticles%2Fwq9vlgx4gygeoskxk1eb.png" alt=" " width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;
&lt;p&gt;iris-medmatch demonstrates how InterSystems IRIS is evolving into a comprehensive AI-Native database. By combining the reliability of FHIR with the power of Vector Search, we can create healthcare applications that truly "understand" the clinical data they store.&lt;/p&gt;

</description>
      <category>github</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>An Introduction to AI Hub, Part 2: Custom MCP Servers</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sat, 30 May 2026 18:39:47 +0000</pubDate>
      <link>https://dev.to/intersystems/an-introduction-to-ai-hub-part-2-custom-mcp-servers-4fol</link>
      <guid>https://dev.to/intersystems/an-introduction-to-ai-hub-part-2-custom-mcp-servers-4fol</guid>
      <description>&lt;p&gt;Welcome back to a series of introductory articles on AI Hub, the new product feature currently in an early access program! (links: &lt;a href="https://evaluation.intersystems.com/Eval/early-access/AIHub" rel="noopener noreferrer"&gt;EAP Site&lt;/a&gt; for download, &lt;a href="https://github.com/intersystems-community/ai-hub-eap/tree/master" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;In the last article, we covered how to create agents and agent tools directly in ObjectScript using the new %AI classes. However, sometimes, instead of creating a new agent, you just want to add some custom tools to an existing agent so you can ask your local claude code, codex, copilot or other agent of choice to query your data directly. This is where MCP Servers might come in.&lt;/p&gt;
&lt;p&gt;In this guide, we will walk through how you can create your own MCP Servers to access your data.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Disclaimer: AI Hub is an early access preview, with features likely to change before production releases, any issues identified can be raised as issues on the documentation GitHub repo linked above. The EAP preview is not to be used in production settings.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;A very brief intro to MCP&lt;/h2&gt;
&lt;p&gt;I'm going to keep this brief because there are loads of other good articles on MCP Servers Model context protocol (I recommend starting with &lt;a href="https://community.intersystems.com/post/model-context-procotol-mcp-intersystems-iris-zero-hero" rel="noopener noreferrer"&gt;this article&lt;/a&gt; from &lt;span&gt;&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/pietro"&gt;@pietro&lt;/a&gt;.DiLeo&lt;/span&gt;&lt;/span&gt; or this &lt;a href="https://www.youtube.com/watch?v=pieK0dog66Q" rel="noopener noreferrer"&gt;brilliant introductory video&lt;/a&gt; from InterSystems President Don Woodlock).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Model Context Protocol is a transport protocol allowing external tools to be added to an agent&lt;/strong&gt;. There is a discovery 'handshake' where the MCP server sends a list of tools to the MCP Client. After the tools are discovered, the agent can send requests for tool executions, including parameters, to the MCP server, which executes the tool call and returns the result.&lt;/p&gt;
&lt;p&gt;MCP servers can be remote servers, i.e. running on a different machine to a client, this usually uses a streamable http/https connection or Server-Side Events. Or MCP servers can be local servers, i.e. running on the same machine, usually using a stdio connection.&lt;/p&gt;
&lt;h3&gt;An important distinction&lt;/h3&gt;
&lt;p&gt;AI hub allows you to create custom MCP servers within your IRIS environment, allowing agents to access or monitor your IRIS databases, productions and statuses. &lt;strong&gt;It is not a pre-configured MCP server&lt;/strong&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;If you are looking for a developer tool which gives your agent free access to an IRIS environment to speed up development, you may be looking for a pre-configured MCP server. If you are looking to create production MCP servers which are secure, auditable and fit within IRIS's governed security environment, AI Hub is what you are looking for.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;There are many pre-configured MCP servers which provides tools for developing with IRIS, including &lt;a href="https://github.com/intersystems-community/iris-agentic-dev" rel="noopener noreferrer"&gt;iris-agentic-dev&lt;/a&gt;, an MCP tool and skills library created by &lt;span&gt;&lt;span&gt;@tomd&lt;/span&gt;&lt;/span&gt;. This is a separate project from AI Hub, so look out for an article about this!&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;MCP in AI Hub&lt;/h2&gt;
&lt;p&gt;In the previous article, we covered creating agent tools and toolsets, here we will go through how to serve these tools as an MCP server using both HTTP and STDIO. The code covered in this article is available in the ai-hub-dev-template which is a nice place to start if you want to play around with IRIS AI hub.&lt;/p&gt;
&lt;p&gt;Before getting to this though lets, take a look at the architecture of an AI Hub MCP server:&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.amazonaws.com%2Fuploads%2Farticles%2Fxjl2trccwf8s9jk6o24n.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.amazonaws.com%2Fuploads%2Farticles%2Fxjl2trccwf8s9jk6o24n.png" alt=" " width="799" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MCP Clients (blue) communicates with &lt;code&gt;iris-mcp-server&lt;/code&gt; to bridge the gap between the MCP calls (discovery and execution) and IRIS. This binary then communicates with an MCP Server web application, defined using &lt;code&gt;%AI.MCP.Service&lt;/code&gt; as a dispatch class. This dispatch class then routes the tool calls to ObjectScript tool classes which can then operate on IRIS databases. This diagram skips the reverse routing (returning the tool responses) as well as the initial handshake between the MCP Client and the &lt;code&gt;iris-mcp-server.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There are more details on this architecture in the documentation, but this simplified view covers the key elements that we need to define. These are:&lt;br&gt;1. Tools / Toolsets&lt;br&gt;2. %AI.MCP.Service dispatch class&lt;br&gt;3. MCP Application&lt;br&gt;4. iris-mcp-server configuration&lt;br&gt;5. MCP Client connection&lt;/p&gt;
&lt;p&gt;Lets go through these one by one.&lt;/p&gt;
&lt;h3&gt;1. Tools&lt;/h3&gt;
&lt;p&gt;We define tools or toolsets by extending %AI.Tool or %AI.ToolSet, this was covered in detail in &lt;a href="https://community.intersystems.com/post/introduction-ai-hub-part-1-agents-objectscript" rel="noopener nofollow noreferrer"&gt;Part 1&lt;/a&gt;, so I'm going to skip over this.&lt;/p&gt;
&lt;h3&gt;2. Defining the dispatch class&lt;/h3&gt;
&lt;p&gt;To define an MCP Service dispatch class, we just need to extend &lt;code&gt;%AI.MCP.Service&lt;/code&gt; and point it at the tools/toolsets we want to include in the &lt;code&gt;SPECIFICATION&lt;/code&gt; parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Sample.MCPService Extends %AI.MCP.Service
{
    Parameter SPECIFICATION = "Sample.ToolSet";
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We could include multiple tool/toolset classes by adding the classes as a comma-separated list, but here we've kept it simple with just one.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;3. Creating the MCP Application&lt;/h3&gt;
&lt;p&gt;Next up, we create an MCP server application. Like other web applications this can be managed from the management portal, or programmatically with the Security.Applications class. In this case, there is a new MCP server management portal in the Management Portal:&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.amazonaws.com%2Fuploads%2Farticles%2F6o7v1ug1yqlxgzfxbvaj.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.amazonaws.com%2Fuploads%2Farticles%2F6o7v1ug1yqlxgzfxbvaj.png" alt=" " width="793" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But everything else will feel familiar to developers creating Web Applications in IRIS.&amp;nbsp;&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.amazonaws.com%2Fuploads%2Farticles%2Fi93aw5uul4yootmclev3.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.amazonaws.com%2Fuploads%2Farticles%2Fi93aw5uul4yootmclev3.png" alt=" " width="799" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key points are to give an endpoint (e.g &lt;code&gt;/mcp/sample&lt;/code&gt;) and the MCP Service Class we created earlier. I won't show the programmatic version, but this can be done with &lt;code&gt;Security.Applications&lt;/code&gt;, just set the &lt;code&gt;Type&lt;/code&gt; value to &lt;code&gt;18&lt;/code&gt; to register it in the MCP Server menu.&lt;/p&gt;
&lt;p&gt;At this point, you can see the JSON description of tools being served at http://localhost:52773/mcp/sample/v1/services. This means it is discoverable by the &lt;code&gt;iris-mcp-server&lt;/code&gt; binary, &lt;strong&gt;it is not discoverable directly by an mcp client.&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;4. iris-mcp-server configuration&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;iris-mcp-server&lt;/code&gt; binary takes a configuration file when it is run, this is run with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iris-mcp-server -c config.toml run&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We'll get to exactly how this command is actually used in the next section (MCP Client), but for now we are going to focus on the config file.&lt;/p&gt;
&lt;p&gt;The first thing to do when writing your config file is to set your connection to IRIS - this requires the credentials for a gateway-privileged user e.g. CSPSystem, the superserver port used for web-gateway (default 1972) and your MCP endpoints:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[iris]]
name = "local"
server = { host = "localhost", port = 1972, username = "SuperUser", password = "SYS" }
endpoints = [
    {path = "/mcp/sample" }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you have to define your transport. This is done in the &lt;code&gt;[mcp]&lt;/code&gt; block.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;stdio&lt;/code&gt;, you just need set the type of transport:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mcp]
transport="stdio"&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For http/https, you also have to give the host and port:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mcp]
transport = "http"
host      = "0.0.0.0"
port      = 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;This port is a different port to the management portal&lt;/strong&gt; and will be used only for this MCP server. This is a common mistake because, although you can find the tool catalog on the management portal port at &lt;a href="http://localhost:52773/mcp/sample/v1/services" rel="noopener noreferrer"&gt;http://localhost:52773/mcp/sample/v1/services&lt;/a&gt;, to actually connect to the MCP server you have to set a different port to communicate to the &lt;code&gt;iris-mcp-server&lt;/code&gt; bridge.&lt;/p&gt;
&lt;h4&gt;Authentication&lt;/h4&gt;
&lt;p&gt;We set the web application to unauthenticated above, but this should be avoided for production use. To add authentication, first set the web application to password authenticated. You can add a username and password or a bearer token to the endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[iris]]
name = "local"
server = { host = "localhost", port = 1972, username = "SuperUser", password = "SYS" }
endpoints = [
    {path = "/mcp/sample", username="SuperUser" password="SYS" }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are using an HTTP/HTTPS connection, you can also choose to not authenticate here, and instead handle authentication in the requests from the MCP client, this will be shown in the connecting from a client setting below.&lt;/p&gt;
&lt;h4&gt;Other settings&lt;/h4&gt;
&lt;p&gt;There are loads more settings to configure here, like the setting up &lt;strong&gt;OAuth&lt;/strong&gt;, using environment &lt;strong&gt;secrets&lt;/strong&gt; rather than hard-coding settings, configuring &lt;strong&gt;logging and telemetry&lt;/strong&gt; and enabling &lt;strong&gt;smart tool discovery&lt;/strong&gt;. To get more details on this, there is a &lt;a href="https://github.com/intersystems-community/ai-hub-eap/blob/master/MCP_Server_Guide.md" rel="noopener nofollow noreferrer"&gt;full guide on the iris-mcp-server usage&lt;/a&gt;, but for basic usage you just need to define &lt;code&gt;[[iris]]&lt;/code&gt; and &lt;code&gt;[mcp]&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;5. Connecting from an MCP Client&lt;/h2&gt;
&lt;p&gt;The method for adding an MCP server will differ depending on which client you are using, but in general there will be an option somewhere in your agent customization settings to add an MCP server. For example, to set up an MCP server on GitHub Copilot, type &lt;code&gt;&amp;gt;MCP: Add Server...&lt;/code&gt; into the VS Code Search bar, or for Claude Code, you can run &lt;code&gt;claude mcp add...&lt;/code&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The first option will likely be a choice between stdio or http(s) transport. These have quite different connection methods so lets tackle them individually.&lt;/p&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;Stdio&lt;/h3&gt;
&lt;p&gt;To use a stdio mcp server, you add &lt;code&gt;/path/to/iris-mcp-server&lt;/code&gt; as the executable. The default location for this in a docker container is &lt;code&gt;/usr/irissys/bin&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You also need to add arguments for the stdio config file &lt;code&gt;config_stdio.toml&lt;/code&gt; and the run command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/usr/irissys/bin/iris-mcp-server -c config_stdio.toml run&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As the authentication is is included in the config file, this is all that is required.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Note, the &lt;code&gt;iris-mcp-server&lt;/code&gt; binary has to be on the same machine (or container) as your MCP client!&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;HTTP&lt;/h3&gt;
&lt;p&gt;To connect to a remote HTTP MCP server from an MCP client, you first need to start the iris-mcp-server transport by opening a shell and running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iris-mcp-server -c config_http.toml run&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unlike the STDIO connection, this needs to be continuously running for the HTTP connection to be usable.&lt;/p&gt;
&lt;p&gt;With this running, we can then add this server to our MCP client by selecting HTTP as the transport or type, and giving the server URL: http://localhost:8080/mcp/sample.&lt;/p&gt;
&lt;p&gt;If we wanted to set authentication at the MCP connection level (rather than the configuration level detailed above), we use standard HTTP authentication headers, like &lt;code&gt;Basic base64(Username:Password)&lt;/code&gt; or &lt;code&gt;Bearer &amp;lt;token&amp;gt;&lt;/code&gt;. The following Python snippet shows an example of connecting to an MCP server using Langchain's &lt;code&gt;MultiServerMCPClient&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import base64
from langchain_mcp_adapters.client import MultiServerMCPClient

AUTH_HEADER = base64.b64encode(b"SuperUser:SYS").decode("utf-8")
async def get_tools():
    client = MultiServerMCPClient(
        {
            "minimal": {
                "transport": "http",
                "url": "http://localhost:8080/mcp/sample",
                "headers": {"Authorization": f"Basic {AUTH_HEADER}"},
            }
        }
    )

    tools = await client.get_tools()
    return tools&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1&gt;Conclusions&lt;/h1&gt;
&lt;p&gt;We've reached the end of Part 2 of this series! It has been a long part, so well done if you have got this far. Hopefully this guide will give you the confidence to use the AI Hub preview to start building your own MCP servers inside IRIS, giving agents secure and governed access to your IRIS instance.&lt;/p&gt;
&lt;p&gt;If you want to see example code from this article, it is all included in the &lt;a href="https://openexchange.intersystems.com/package/ai-hub-dev-template" rel="noopener noreferrer"&gt;ai-hub-dev-template&lt;/a&gt; on open exchange. This is an example docker project that you can easily clone and use to start working with AI Hub on your local machine. In it, there is sample MCP server and as well as example code to programmatically create the MCP Server Web Application.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>New SMART on FHIR v2 Scopes</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 24 May 2026 15:13:59 +0000</pubDate>
      <link>https://dev.to/intersystems/new-smart-on-fhir-v2-scopes-e3j</link>
      <guid>https://dev.to/intersystems/new-smart-on-fhir-v2-scopes-e3j</guid>
      <description>&lt;p&gt;In v2026.1 we introduced support for a more robust and real-life secure authorization for your FHIR endpoints.&lt;/p&gt;
&lt;p&gt;This is achieved by using &lt;a href="https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=HXFHIRADM_server_auth#HXFHIRADM_server_auth_oauth_scopes" rel="noopener noreferrer"&gt;SMART on FHIR v2 fine-grained scopes&lt;/a&gt;.&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.amazonaws.com%2Fuploads%2Farticles%2Fjt7urawsgh29q7so1ca0.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.amazonaws.com%2Fuploads%2Farticles%2Fjt7urawsgh29q7so1ca0.png" alt=" " width="800" height="435"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h2&gt;Focus - Not SMART in general, rather, the fine-grained scopes; Hands-on easy sample&lt;/h2&gt;
&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;p&gt;I have dived into the topic of SMART on FHIR in the past, for example see &lt;a href="https://community.intersystems.com/post/smart-fhir-app-sample-hands-exerciseworkshop-instructions" rel="noopener noreferrer"&gt;this article&lt;/a&gt; I wrote (with an accompanying &lt;a href="https://openexchange.intersystems.com/package/smart-day-hands-on" rel="noopener noreferrer"&gt;Open Exchange app&lt;/a&gt;, and &lt;a href="https://www.youtube.com/watch?v=OHaZ5qiyQ1c" rel="noopener noreferrer"&gt;related video series&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Also others have discussed this topic, for example &lt;span&gt;&lt;span&gt;@LuisAngel.PérezRamos&lt;/span&gt;&lt;/span&gt; in his &lt;a href="https://community.intersystems.com/post/developing-smart-fhir-applications-auth0-and-intersystems-iris-fhir-server-introduction" rel="noopener noreferrer"&gt;Developing SMART On FHIR Applications with Auth0 and InterSystems IRIS FHIR Server&lt;/a&gt; article series, &lt;a class="mentioned-user" href="https://dev.to/nicole"&gt;@nicole&lt;/a&gt;.Sun&lt;span&gt;&lt;span&gt;&amp;nbsp;in her&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href="https://community.intersystems.com/post/smart-fhir-ehr-launch-iris-health" rel="noopener noreferrer"&gt;SMART on FHIR EHR Launch with IRIS for Health&lt;/a&gt; article, and &lt;a class="mentioned-user" href="https://dev.to/kate"&gt;@kate&lt;/a&gt;.Lau&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;in her two-part&amp;nbsp;&lt;a href="https://community.intersystems.com/post/using-postman-testing-oauth20-intersystems-fhir-repository-part1" rel="noopener noreferrer"&gt;Using Postman for testing the OAuth2.0 of the InterSystems FHIR repository&lt;/a&gt;.&lt;/p&gt;In addition this Learning Services video - &lt;a href="https://www.youtube.com/watch?v=wAB-msyXq_8" id="OWAb43c6521-1114-4055-9f43-89a408a783e1" rel="noopener noreferrer"&gt;Configuring OAuth for InterSystems FHIR Server&lt;/a&gt;&amp;nbsp;- explains this nicely, and even demonstrates part of the latest SMART scope-based result filtering that we'll discuss here.But in the above mentioned articles and samples, we either used InterSystems IRIS itself as the OAuth Server, or a 3rd party cloud OAuth Server (like auth0 by Okta), but in this article and sample I want to do a few things differently -&lt;p&gt;1. I want to use a 3rd party OAuth Server, but not one you will need to register (and perhaps pay) for. This will be &lt;a href="https://www.keycloak.org/" rel="noopener noreferrer"&gt;&lt;strong&gt;Keycloak&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;2. I want to take care of all of the setup for you in a &lt;strong&gt;Dockerized sample&lt;/strong&gt; -&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;&lt;em&gt;InterSystems IRIS for Health&lt;/em&gt;&lt;/span&gt; up an running with a FHIR Endpoint defined, including an OAuth client defined, and some Resources in the Repository.&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;&lt;em&gt;Keycloak &lt;/em&gt;&lt;/span&gt;up and running with a client corresponding to the IRIS OAuth client.&lt;/li&gt;
&lt;li&gt;A &lt;span&gt;&lt;em&gt;Postman &lt;/em&gt;&lt;/span&gt;Collection to allow for quick testing and demonstration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3. I want to focus on the relatively newer &lt;strong&gt;fine-grained SMART scopes&lt;/strong&gt;, not the basic ones. This is really the crux of the matter here. The other two above, are enablers for letting us focus just on this item.&lt;/p&gt;
&lt;h2&gt;SMART Scopes - The Granular Fine-grained Version&lt;/h2&gt;
&lt;p&gt;Above I generated (thank you NotebookLM) a nice infographic that summarizes the general syntax and usage of SMART scopes.&lt;/p&gt;
&lt;p&gt;In particular I want to focus on the filter part, the part in the scopes from the question mark (?).&lt;/p&gt;
&lt;p&gt;Here you can use standard FHIR Search syntax, with standard FHIR Search Parameters.&lt;/p&gt;
&lt;p&gt;Let's take this example:&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.amazonaws.com%2Fuploads%2Farticles%2Fq5n17wu4jx19gnypxukg.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.amazonaws.com%2Fuploads%2Farticles%2Fq5n17wu4jx19gnypxukg.png" alt=" " width="799" height="107"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of just allowing access (Read &amp;amp; Search in this case) to all categories of Observations, here we are allowing only access to lab results (category=laboratory).&lt;/p&gt;
&lt;p&gt;So to illustrate, instead of getting access to a set like this:&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.amazonaws.com%2Fuploads%2Farticles%2Fpb9gm49k6et2pd63kmro.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.amazonaws.com%2Fuploads%2Farticles%2Fpb9gm49k6et2pd63kmro.png" alt=" " width="800" height="714"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can limit the access to a set like this:&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.amazonaws.com%2Fuploads%2Farticles%2Fcmg0nc9x3461rasx2teq.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.amazonaws.com%2Fuploads%2Farticles%2Fcmg0nc9x3461rasx2teq.png" alt=" " width="800" height="705"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This allows for an ABAC (Attribute-Based Access Control) approach to access FHIR data (see more about this topic in the &lt;a href="https://build.fhir.org/security.html#binding" rel="noopener noreferrer"&gt;FHIR docs&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Some local national regulations mandate enforcing this kind of access control, including for example using security tags to limit access to data.&lt;/p&gt;
&lt;p&gt;One example is the ONC certification in the US, but other countries have similar demands.&lt;/p&gt;
&lt;p&gt;So supporting this is not only important for securing your data, it is also a hard requirement by local law, in a growing number of places.&lt;/p&gt;
&lt;h2&gt;The Power to Filter (or Not to)&lt;/h2&gt;
&lt;p&gt;You can control whether, if the FHIR request does not adhere exactly to the scopes, to filter out the unauthorized data and return just what is allowed, or to fail the request and return a 403 error HTTP status.&lt;/p&gt;
&lt;p&gt;This setting is in the FHIR endpoint Authorization settings:&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.amazonaws.com%2Fuploads%2Farticles%2Ft058qt6elitfrzr2jluw.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.amazonaws.com%2Fuploads%2Farticles%2Ft058qt6elitfrzr2jluw.png" alt=" " width="799" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note this is relevant not only to fine-grained scopes but also without using the ? filter. For example if you use _include or the $everything operation, this could filter "whole" Resource Types from the Result Set.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Here's an example to illustrate -&lt;/p&gt;
&lt;h4&gt;Observation Search Example&lt;/h4&gt;
&lt;p&gt;Say we issue a Search for Observations, using Basic Authentication, so no SMART Scope are applied.&lt;/p&gt;
&lt;p&gt;You can see here we are getting back 793 Resources.&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.amazonaws.com%2Fuploads%2Farticles%2F0bt29oujkxpe55h8wqmf.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.amazonaws.com%2Fuploads%2Farticles%2F0bt29oujkxpe55h8wqmf.png" alt=" " width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Looking a little closer we can see for example the first one has a category of vital-signs:&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.amazonaws.com%2Fuploads%2Farticles%2F1eypyi5t0u33kjl0ckwl.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.amazonaws.com%2Fuploads%2Farticles%2F1eypyi5t0u33kjl0ckwl.png" alt=" " width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And in comparison if I use OAuth 2 authentication and have a scope of user/Observation.rs?category=laboratory, we get only 385 (vs. 793 above) Resources:&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.amazonaws.com%2Fuploads%2Farticles%2Fdzelpiswoe3m8426i671.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.amazonaws.com%2Fuploads%2Farticles%2Fdzelpiswoe3m8426i671.png" alt=" " width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;And the first one (instead of vital-signs) is usurpingly of a category of laboratory:&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.amazonaws.com%2Fuploads%2Farticles%2Fw5zcmyrm8cltdtx6lm2y.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.amazonaws.com%2Fuploads%2Farticles%2Fw5zcmyrm8cltdtx6lm2y.png" alt=" " width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A similar comparison can been seen with $everything -&lt;/p&gt;
&lt;h4&gt;$everyting Example&lt;/h4&gt;
&lt;p&gt;With Basic Authentication (no Scopes):&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.amazonaws.com%2Fuploads%2Farticles%2Fmjlpi45fufagp91fb78q.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.amazonaws.com%2Fuploads%2Farticles%2Fmjlpi45fufagp91fb78q.png" alt=" " width="799" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We get of course the Patient Resource itself, but also related Resources (per the example above): Encounter, Practitioner, Organization, Condition, Claim, ExplanationOfBenefit, Observation (of various types), MedicationRequest, Immunization, DiagnosticReport&lt;/p&gt;
&lt;p&gt;With OAuth (and scopes that include only: user/Patient.rs and user/Observation.rs?category=laboratory):&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.amazonaws.com%2Fuploads%2Farticles%2F3ahgk6ynaahkv24bposl.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.amazonaws.com%2Fuploads%2Farticles%2F3ahgk6ynaahkv24bposl.png" alt=" " width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here apart from the Patient itself, we only get Observations (laboratory ones), and no other related Resources.&lt;/p&gt;
&lt;p&gt;So, 171 Resources vs. 35 after the filtering.&lt;/p&gt;
&lt;h2&gt;Some Technical Notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;As mentioned you need to be at least on v2026.1 to support this.&lt;/li&gt;
&lt;li&gt;Most FHIR Interactions are supported for these kind of Scopes (Create, Read, Update, Delete, Search), some not yet (History, VRead)&lt;/li&gt;
&lt;li&gt;As mentioned in the filter search string you can use standard FHIR Search syntax, but some parameters simply won't make sense in this context (like _include), so some might fail the request and others might simply be ignored, see referenced Docs for details.&lt;/li&gt;
&lt;li&gt;There are some notes re the $everything and $lastn, again see Docs for details.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Debugging&lt;/h2&gt;
&lt;p&gt;While using OAuth in general, and with SMART scopes in particular, not everything will work always as expected at first.&lt;/p&gt;
&lt;p&gt;Good resources to debug your situation will be the FHIR Server Log (aka FSLOG) and the HTTP Request Log (aka ISCLOG), see more details in &lt;a href="https://docs.intersystems.com/irisforhealth20261/csp/docbook/DocBook.UI.Page.cls?KEY=HXFHIRADM_server_debugMaintain#HXFHIRADM_server_debug_log" rel="noopener noreferrer"&gt;the Docs here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To illustrate here's an example -&lt;/p&gt;
&lt;p&gt;Say this time we turned off the filter results settings, and we're trying to Search for all Observation while our scope allows only laboratory.&lt;/p&gt;
&lt;p&gt;We will get a 403 Forbidden HTTP status:&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.amazonaws.com%2Fuploads%2Farticles%2Fpm6ihjxkoaa48t6qxj3l.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.amazonaws.com%2Fuploads%2Farticles%2Fpm6ihjxkoaa48t6qxj3l.png" alt=" " width="799" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if we turned on the FHIR Server Log, we can see something like this:&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.amazonaws.com%2Fuploads%2Farticles%2Fce2g0jfrh9xgajiv5py7.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.amazonaws.com%2Fuploads%2Farticles%2Fce2g0jfrh9xgajiv5py7.png" alt=" " width="799" height="48"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fd6odvcizcas74buyipml.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.amazonaws.com%2Fuploads%2Farticles%2Fd6odvcizcas74buyipml.png" alt=" " width="794" height="41"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Sample Demo&lt;/h2&gt;
&lt;p&gt;Tackling a topic hands-on always helps and deepens the understanding, so I encourage you to take the related Open Exchange app for a ride. It is a very simple click &amp;amp; go sample, where a docker compose will build and start up everything you need, and includes a sample Postman Collection for you test drive with.&lt;/p&gt;
&lt;p&gt;Here's a recording of the demo from a READY 2026 session:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.intersystems.com/smarter-scopes-in-action-live-demo-of-smart-v2-with-is-fhir-server-intersystems/" rel="noopener noreferrer"&gt;SMARTer Scopes in Action - Live Demo of SMART v2 with InterSystems FHIR Server&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>oauth</category>
      <category>security</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
