<?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: Kate Steele</title>
    <description>The latest articles on DEV Community by Kate Steele (@kate_steeleeee).</description>
    <link>https://dev.to/kate_steeleeee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4076214%2Fa1d02646-5fa5-4cb0-9cd8-7d8471a93384.png</url>
      <title>DEV Community: Kate Steele</title>
      <link>https://dev.to/kate_steeleeee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kate_steeleeee"/>
    <language>en</language>
    <item>
      <title>Isolation Forest Anomaly Detection on Login Logs in Python: Thresholds, Explanations, Drift</title>
      <dc:creator>Kate Steele</dc:creator>
      <pubDate>Tue, 03 Nov 2026 15:28:09 +0000</pubDate>
      <link>https://dev.to/kate_steeleeee/isolation-forest-anomaly-detection-on-login-logs-in-python-thresholds-explanations-drift-1gpo</link>
      <guid>https://dev.to/kate_steeleeee/isolation-forest-anomaly-detection-on-login-logs-in-python-thresholds-explanations-drift-1gpo</guid>
      <description>&lt;p&gt;Stolen passwords just lost first place. Verizon’s 2026 Data Breach Investigations Report says 31% of breaches now start with software vulnerabilities, “beating stolen passwords as the top way attackers get in.” The same report still lists stolen credentials among the most frequent causes of breaches, and those attacks leave a trail in one boring table: the authentication log.&lt;/p&gt;

&lt;p&gt;The pitch for machine learning here is familiar. Signature rules only catch what someone already wrote a rule for, and an unsupervised model learns “normal” and flags the rest. I wanted to see what that looks like on login data with real numbers, so I built the whole loop with &lt;strong&gt;Isolation Forest anomaly detection&lt;/strong&gt; in Python and scikit-learn: a synthetic auth log with labeled attacks, per-user features, training, threshold selection, evaluation against a hand-written rule baseline, per-alert explanations, and a drift scenario that forces a retrain.&lt;/p&gt;

&lt;p&gt;Short version of the results: the rules won on precision (0.906 vs 0.426), the forest won on recall (0.894 vs 0.780), and the forest was the only one that caught any of the quietest attack. Every number below comes from a real run of the anomaly detection Python code in this post.&lt;/p&gt;

&lt;p&gt;What we’ll build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A log generator with a fixed seed and four labeled attack types&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Per-user, per-hour features that only look backward in time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An Isolation Forest and three ways to set its threshold&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Precision, recall and PR-AUC, plus a comparison with SIEM-style rules&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Why did this fire?” explanations for single alerts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A drift week that breaks the threshold, and two ways to fix it&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stack: Python 3.13.18, scikit-learn 1.9.1, pandas 3.0.5, numpy 2.5.3, pytest 9.1.1. The full experiment runs in well under a minute on 2 CPUs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the attacks look like in an auth log
&lt;/h2&gt;

&lt;p&gt;Before choosing among anomaly detection algorithms, it helps to write down what each attack leaves behind. These are the four I generate, mapped to MITRE ATT&amp;amp;CK where it applies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Attack&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;What the log shows&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Feature that should notice&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Credential stuffing (T1110.004)&lt;/td&gt;
&lt;td&gt;A few datacenter IPs try 1-2 passwords on many accounts within an hour&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ip_fanout&lt;/code&gt;, &lt;code&gt;new_device&lt;/code&gt;, &lt;code&gt;n_fail&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brute force, then takeover&lt;/td&gt;
&lt;td&gt;10-39 failures on one account, then a success from a new device abroad&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;n_fail&lt;/code&gt;, &lt;code&gt;fail_ratio&lt;/code&gt;, &lt;code&gt;new_country&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impossible travel&lt;/td&gt;
&lt;td&gt;A clean login from another continent shortly after the real user’s login&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;geo_speed_kmh&lt;/code&gt;, &lt;code&gt;new_device&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low-and-slow stuffing&lt;/td&gt;
&lt;td&gt;One failed try per account from a residential IP in the victim’s own city&lt;/td&gt;
&lt;td&gt;almost nothing, by design&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OWASP describes credential stuffing as “the automated injection of stolen username and password pairs” and notes it works because people reuse passwords. MITRE’s detection guidance for T1110.004 starts where you’d expect: rapid failures across different users from one IP. Identity providers ship their own versions of the travel signal: Microsoft Entra ID Protection, for example, has both “atypical travel” and “impossible travel” risk detections. The fourth attack is there to keep the experiment honest. A careful attacker spreads attempts across residential proxies and stays under every per-IP and per-account threshold.&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%2Flgllql2xv8j85u87frjf.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%2Flgllql2xv8j85u87frjf.png" alt=" " width="800" height="480"&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/media%2Fimage1.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/media%2Fimage1.png" alt="Pipeline from raw login events to hourly per-user windows, ten features, Isolation Forest score, alert-budget threshold and an explained alert, with a retraining loop" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pipeline from raw login events to hourly per-user windows, ten features, Isolation Forest score, alert-budget threshold and an explained alert, with a retraining loop&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Generate auth logs with labeled attacks
&lt;/h2&gt;

&lt;p&gt;Real labeled login data is hard to get and impossible to share, so the generator does three things on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Normal users have habits.&lt;/strong&gt; Each of 400 users has a home city, 1-3 home IPs, 1-2 devices, a typical login hour and a login rate. About 8% of logins come from a mobile IP and 4% start with a mistyped password.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Normal users also do odd things.&lt;/strong&gt; Some take business trips, buy new phones or forget their password and fail 4-8 times in a row. These are the false positives any detector has to live with.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The training period is not clean.&lt;/strong&gt; Days 1-21 contain a quarter-scale copy of the attacks, unlabeled, because your historical logs aren’t clean either.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The attack injector is the interesting part:&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;attacks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&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="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;n&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;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;max&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;int&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="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="c1"&gt;# 1) Credential stuffing: a few datacenter IPs, many users, 1-2 tries each, rare success.
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&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="nf"&gt;n&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="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_to&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;hours&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="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;ips&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;45.95&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;_&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="mi"&gt;3&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;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;n&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;replace&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;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&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="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&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;_&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="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&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;3&lt;/span&gt;&lt;span class="p"&gt;))):&lt;/span&gt;
                &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;
                &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Frankfurt&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;bot-&lt;/span&gt;&lt;span class="sh"&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1e6&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                                   &lt;span class="mi"&gt;16509&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credential_stuffing&lt;/span&gt;&lt;span class="sh"&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="c1"&gt;# 2) Brute force -&amp;gt; takeover: many failures on one account, then success from a new device abroad.
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;n&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;replace&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;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_to&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;hours&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="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ATTACK_CITIES&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;91.200&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;k&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="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&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;40&lt;/span&gt;&lt;span class="p"&gt;))):&lt;/span&gt;
            &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&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;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;atk-dev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brute_force&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;atk-dev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9123&lt;/span&gt;&lt;span class="p"&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;brute_force&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# 3) Impossible travel: phished password used from another continent, no failures at all.
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;n&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="n"&gt;replace&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;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_to&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_hour&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="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&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;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ips&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;normal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;102.89&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ATTACK_CITIES&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;atk-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                           &lt;span class="mi"&gt;36873&lt;/span&gt;&lt;span class="p"&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;impossible_travel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# 4) Low-and-slow stuffing via residential proxies in the victim's own city: one try per user.
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;n&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;replace&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;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_to&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_hour&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;73.&lt;/span&gt;&lt;span class="sh"&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&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;99&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                           &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low_and_slow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;«Full generator (users, normal traffic, benign oddities)»:&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;make_users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_users&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&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;u&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;n_users&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HOME_CITIES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[.&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&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="mi"&gt;12&lt;/span&gt;&lt;span class="p"&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="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&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;u&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="n"&gt;d&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ips&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="nf"&gt;_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&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;73.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&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;4&lt;/span&gt;&lt;span class="p"&gt;))],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devices&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;d&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="n"&gt;d&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&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;3&lt;/span&gt;&lt;span class="p"&gt;))],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;7922&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;701&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20115&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5650&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2856&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3320&lt;/span&gt;&lt;span class="p"&gt;])),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_hour&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;   &lt;span class="c1"&gt;# UTC
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;      &lt;span class="c1"&gt;# logins per weekday
&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;users&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normal_traffic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day0&lt;/span&gt;&lt;span class="o"&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;rows&lt;/span&gt; &lt;span class="o"&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;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;users&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;d&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;day0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;weekend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="n"&gt;dayofweek&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poisson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate&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="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;weekend&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;))):&lt;/span&gt;
                &lt;span class="n"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_hour&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;
                &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ips&lt;/span&gt;&lt;span class="sh"&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;172.56&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# mobile
&lt;/span&gt;                &lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devices&lt;/span&gt;&lt;span class="sh"&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.04&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# typo, then success
&lt;/span&gt;                    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&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;60&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;normal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;benign_oddities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_to&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Legit behavior that *looks* suspicious: trips, new phones, forgotten passwords.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&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;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;max&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;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;replace&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="c1"&gt;# business trips
&lt;/span&gt;        &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_to&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;HOME_CITIES&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&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="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Singapore&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;Warsaw&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;k&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;+&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;hours&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="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&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="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;185.20&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;9009&lt;/span&gt;&lt;span class="p"&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;normal&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;trip&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;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;max&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;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;replace&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="c1"&gt;# new phone
&lt;/span&gt;        &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_to&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_hour&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;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ips&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&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="s"&gt;-new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;normal&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;new_phone&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;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;max&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;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;replace&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="c1"&gt;# forgot password
&lt;/span&gt;        &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_to&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_hour&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;24&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;k&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="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;))):&lt;/span&gt;
            &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&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;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ips&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                               &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normal&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;forgot_password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&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;n_users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;train_days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_days&lt;/span&gt;&lt;span class="o"&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;Days [0, train_days) are &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;history&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; (with a little unlabeled attack noise), the rest is test.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;train_days&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;test_days&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normal_traffic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;benign_oddities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&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;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;attacks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&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="n"&gt;train_days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# contamination nobody labeled
&lt;/span&gt;    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;attacks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;train_days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&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;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reset_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drop&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;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;is_attack&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;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;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&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;users&lt;/span&gt;
&lt;span class="n"&gt;python&lt;/span&gt; &lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scikit&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;learn&lt;/span&gt; &lt;span class="mf"&gt;1.9&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="n"&gt;pandas&lt;/span&gt; &lt;span class="mf"&gt;3.0&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="n"&gt;numpy&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;481&lt;/span&gt;  &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
&lt;span class="n"&gt;train&lt;/span&gt; &lt;span class="nf"&gt;windows &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;21&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;023&lt;/span&gt;  &lt;span class="n"&gt;unlabeled&lt;/span&gt; &lt;span class="n"&gt;attack&lt;/span&gt; &lt;span class="n"&gt;windows&lt;/span&gt; &lt;span class="n"&gt;hiding&lt;/span&gt; &lt;span class="n"&gt;inside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="nf"&gt;windows  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;28&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;403&lt;/span&gt;
&lt;span class="n"&gt;label&lt;/span&gt;
&lt;span class="n"&gt;normal&lt;/span&gt;                 &lt;span class="mi"&gt;5280&lt;/span&gt;
&lt;span class="n"&gt;credential_stuffing&lt;/span&gt;      &lt;span class="mi"&gt;80&lt;/span&gt;
&lt;span class="n"&gt;low_and_slow&lt;/span&gt;             &lt;span class="mi"&gt;25&lt;/span&gt;
&lt;span class="n"&gt;impossible_travel&lt;/span&gt;        &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;brute_force&lt;/span&gt;               &lt;span class="mi"&gt;8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test week has 123 attack windows out of 5,403, a base rate of 2.3%. Keep that number in mind: a detector that flags everything gets precision 0.023.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Feature engineering per user and time window
&lt;/h2&gt;

&lt;p&gt;This isn’t classic time series anomaly detection on a single metric. Each row is one user in one clock hour, and every feature compares that hour with what the user did &lt;em&gt;before&lt;/em&gt; it. Getting the “before” right matters more than the model choice.&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;build_windows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Aggregate events into (user, 1h) windows. Returns (windows, history) so the
    per-user state can be carried into the next batch (train -&amp;gt; test -&amp;gt; drift).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&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;ev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window&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;ev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Network-side feature: how many distinct users each source IP touched in that hour.
&lt;/span&gt;    &lt;span class="n"&gt;fanout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip&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;window&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;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;nunique&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip_users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&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;fanout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&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;ip&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;window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&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;window&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;user&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;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;recs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;recs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window&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;recs&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;window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;recs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&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;recs&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;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recs&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="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;recs&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="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&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;devices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="sh"&gt;"&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="c1"&gt;# Geo-velocity for every event against the previous *successful* login, including
&lt;/span&gt;        &lt;span class="c1"&gt;# successes earlier in the same window. My first version checked only the first event
&lt;/span&gt;        &lt;span class="c1"&gt;# of the window and read 0 km/h when the attacker logged in right after the real user.
&lt;/span&gt;        &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&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;prev&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;km&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;haversine_km&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&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="n"&gt;prev&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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                &lt;span class="n"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&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;prev&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="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 15-min floor for IP-geo noise
&lt;/span&gt;                &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;km&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;hours&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;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;fails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;attack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_attack&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
        &lt;span class="n"&gt;seen&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;known&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;known&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&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_attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&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_fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fails&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail_ratio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fails&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&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_ips&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new_device&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;device&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devices&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;new_country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&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;new_asn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asns&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;geo_speed_kmh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hour_rarity&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip_fanout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip_users&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_attack&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attack&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attack&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normal&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;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&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="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="c1"&gt;# Update state only from *successful* logins: failed attempts must not teach the
&lt;/span&gt;        &lt;span class="c1"&gt;# model that the attacker's device or country is "known".
&lt;/span&gt;        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;device&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&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;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ok&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;lat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ok&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;lon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;hist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three decisions in there are worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;State updates only from successful logins.&lt;/strong&gt; If failed attempts updated “known devices,” a brute-force attacker would teach the model their own device during the failures and look familiar by the time they succeed. There’s a test for this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Geo-velocity is checked for every event in the window.&lt;/strong&gt; My first version only checked the first event of each hour. When the attacker logged in right after the real user in the same hour, it compared the user with themselves and returned 0 km/h. The fixed version reports 21,034 km/h for that window.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ip_fanout&lt;/code&gt; &lt;strong&gt;is the one network-level feature.&lt;/strong&gt; It counts how many distinct users a source IP touched in that hour. Network anomaly detection usually starts from exactly this kind of cross-account view, and it’s what MITRE recommends for stuffing.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Heavy-tailed columns go through &lt;code&gt;log1p&lt;/code&gt; before training:&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;to_matrix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Heavy-tailed columns go through log1p (Isolation Forest draws split values uniformly
    between a node&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s min and max). In this dataset it barely moved PR-AUC; see gotchas.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FEATURES&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&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_attempts&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;n_fail&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;n_ips&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;geo_speed_kmh&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;ip_fanout&lt;/span&gt;&lt;span class="sh"&gt;"&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;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log1p&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;c&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;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3. Train the Isolation Forest
&lt;/h2&gt;

&lt;p&gt;The idea behind Isolation Forest, from Liu, Ting and Zhou’s 2008 paper, is that anomalies are “few and different.” Each tree picks a random feature and a random split value between that feature’s min and max, and keeps splitting. Rare points get isolated after a few splits, so their average path length across the forest is short. The scikit-learn docs put it as “noticeably shorter paths for anomalies.” The paper used 256-point subsamples and 100 trees. I use 300 trees for steadier scores; &lt;code&gt;max_samples=256&lt;/code&gt; matches scikit-learn’s &lt;code&gt;"auto"&lt;/code&gt; setting at this data size.&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;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contamination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&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;7&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;IsolationForest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_estimators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_samples&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contamination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;contamination&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;seed&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_train&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;anomaly_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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Higher = more anomalous. score_samples() returns the opposite sign.&lt;/span&gt;&lt;span class="sh"&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;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score_samples&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;p&gt;One sign convention trips people up: &lt;code&gt;score_samples()&lt;/code&gt; returns the &lt;em&gt;opposite&lt;/em&gt; of the paper’s score, and lower means more abnormal. I flip it once so that “higher = worse” everywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4. Pick a threshold: contamination vs.&amp;nbsp;score threshold
&lt;/h2&gt;

&lt;p&gt;This is where most Isolation Forest tutorials stop at &lt;code&gt;contamination=0.01&lt;/code&gt; and move on. Here are four options on the same trained model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contamination='auto'  offset_ = -0.5 | alerts= 1080  precision=0.114  recall=1.000  PR-AUC=0.873
contamination=0.01    offset_ = -0.695 | 1st percentile of train score_samples = -0.695
contamination=0.01    alerts=  151  precision=0.669  recall=0.821  PR-AUC=0.873
budget  5/day -&amp;gt; score&amp;gt;=0.705   alerts=  132  precision=0.750  recall=0.805  PR-AUC=0.873
budget 10/day -&amp;gt; score&amp;gt;=0.691   alerts=  162  precision=0.623  recall=0.821  PR-AUC=0.873
budget 20/day -&amp;gt; score&amp;gt;=0.652   alerts=  258  precision=0.426  recall=0.894  PR-AUC=0.873
budget 40/day -&amp;gt; score&amp;gt;=0.617   alerts=  352  precision=0.321  recall=0.919  PR-AUC=0.873
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the output says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;contamination="auto"&lt;/code&gt; &lt;strong&gt;is not “let the model decide.”&lt;/strong&gt; The scikit-learn docs say the offset is fixed at -0.5, as in the original paper. On this data that means 1,080 alerts in a week, a 20% alert rate. The paper itself warns that when every score sits near 0.5, there may be no clear anomalies, and with mostly discrete, low-variance features many normal windows score near that line.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;contamination=0.01&lt;/code&gt; &lt;strong&gt;is just a quantile of the training scores.&lt;/strong&gt; The fitted &lt;code&gt;offset_&lt;/code&gt; (-0.695) equals the 1st percentile of &lt;code&gt;score_samples&lt;/code&gt; on the training set, to three decimals. It’s not a statement about how many attacks exist. It’s a statement about how many alerts you’ll get on data like your training data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;So set it from your alert budget.&lt;/strong&gt; If the SOC can triage 20 login alerts a day, compute the cut-off that produced 20/day on the training period and use that score threshold directly:&lt;br&gt;
&lt;/p&gt;&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;threshold_for_budget&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;alerts_per_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_days&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Score cut-off that yields a fixed number of alerts on a reference period.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alerts_per_day&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n_days&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&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="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;k&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 20/day the model produced 258 alerts in the test week: about 21 false positives per day, which matches the budget, plus the attacks on top. PR-AUC doesn’t depend on any threshold, which is why it’s identical in every line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5. Evaluate with precision, recall and PR-AUC
&lt;/h2&gt;

&lt;p&gt;With 2.3% positives, ROC-AUC can look great while most alerts are noise, so I report precision, recall and PR-AUC. scikit-learn’s &lt;code&gt;average_precision_score&lt;/code&gt; computes it as a recall-weighted mean of precisions, which “is not interpolated” and avoids the optimism of the trapezoidal rule. The model scores 0.873 against a no-skill baseline of 0.023.&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%2Fpjkctysdxtud2azqs69o.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%2Fpjkctysdxtud2azqs69o.png" alt=" " width="800" height="480"&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/media%2Fimage4.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/media%2Fimage4.png" alt="Precision-recall curve of the Isolation Forest on the test week, with operating points for contamination auto, contamination 0.01, the 20-alerts-per-day budget, and the rule baseline sitting above the curve" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Precision-recall curve of the Isolation Forest on the test week, with operating points for contamination auto, contamination 0.01, the 20-alerts-per-day budget, and the rule baseline sitting above the curve&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6. Compare with a rule baseline
&lt;/h2&gt;

&lt;p&gt;An anomaly detection machine learning model should beat what a security engineer writes in ten minutes. Here’s that ten-minute version:&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;rule_baseline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;What a SIEM correlation rule would typically do.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;w&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_fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;geo_speed_kmh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new_country&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="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new_device&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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip_fanout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;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;to_numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five failures in an hour is an &lt;em&gt;alert&lt;/em&gt;, not a lockout. For comparison, NIST SP 800-63B only requires the verifier to cap consecutive failed attempts on an account at no more than 100.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rules             alerts=  106  precision=0.906  recall=0.780
iforest@20/day    alerts=  258  precision=0.426  recall=0.894
rules OR iforest  alerts=  267  precision=0.412  recall=0.894

attack windows caught, by type:
                     rules  iforest  total
label                                     
brute_force              8        8      8
credential_stuffing     78       80     80
impossible_travel       10       10     10
low_and_slow             0       12     25

false positives of iforest@20/day by cause:
note
plain normal    144
new_phone         3
trip              1
  of the plain-normal ones: 72% have 3+ logins in the hour, 81% contain a mistyped password
false positives of rules by cause: {'plain normal': 6, 'trip': 4}
  rule false positives where the user's previous successful login was the attacker's: 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rules win on precision by a wide margin: 96 of their 106 alerts are real. They catch every brute-force and impossible-travel window and 78 of 80 stuffing windows. The forest catches all three of those types too, but pays for it with 148 false positives. Its one real advantage is the bottom row: it caught 12 of the 25 low-and-slow attempts. The rules caught none.&lt;/p&gt;

&lt;p&gt;The false positives are revealing. The forest’s top complaint is “busy hour with a typo”: 72% of its plain-normal false positives have 3+ logins in one hour and 81% include a mistyped password. That’s rare, so the forest flags it, but rare isn’t the same as malicious. Four of the rules’ ten false positives are more interesting: the victim’s &lt;em&gt;own&lt;/em&gt; next login after a takeover. Their “last known location” was now the attacker’s, so coming home looked like impossible travel. Under the labels these count as false positives, but an analyst would want to see them.&lt;/p&gt;

&lt;p&gt;Two caveats before anyone quotes “rules beat ML.” First, I wrote the rules &lt;em&gt;and&lt;/em&gt; the generator, so the rules know exactly what the attacks look like. That’s the best case for rules, and real attackers don’t send you their source code. Second, &lt;code&gt;rules OR iforest&lt;/code&gt; adds nothing here, because every attack the rules catch is already in the forest’s top 258. A more useful hybrid is rules for paging and the forest’s top-N as a daily review queue for what the rules can’t express.&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%2Fxdfljljj7g7fl4wt82td.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%2Fxdfljljj7g7fl4wt82td.png" alt=" " width="800" height="480"&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/media%2Fimage2.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/media%2Fimage2.png" alt="Matrix of attack types against detectors: rules and Isolation Forest catch counts per attack type, plus the top false-positive patterns for each" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Matrix of attack types against detectors: rules and Isolation Forest catch counts per attack type, plus the top false-positive patterns for each&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7. Explain an alert
&lt;/h2&gt;

&lt;p&gt;An alert that says “score 0.784” gets ignored. The cheapest model-agnostic explanation I know is ablation: reset one feature at a time to its training median and measure how much the anomaly score drops.&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;explain&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_train&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;top&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Ablation: reset one feature at a time to its training median and see how much the
    anomaly score drops. Big drop = this feature is what made the window look odd.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;med&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&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;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;anomaly_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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;probes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:],&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FEATURES&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&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;probes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FEATURES&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FEATURES&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;med&lt;/span&gt;
    &lt;span class="n"&gt;drops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;anomaly_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;probes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drops&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="n"&gt;top&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;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;FEATURES&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;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drops&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="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="n"&gt;order&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;drops&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="o"&gt;&amp;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;u0090&lt;/span&gt; &lt;span class="mi"&gt;09&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&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;impossible_travel&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.784&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;yes&lt;/span&gt;
    &lt;span class="n"&gt;new_device&lt;/span&gt;     &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;      &lt;span class="mf"&gt;1.0&lt;/span&gt;  &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;typical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.041&lt;/span&gt;
    &lt;span class="n"&gt;geo_speed_kmh&lt;/span&gt;  &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="mf"&gt;21033.8&lt;/span&gt;  &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;typical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.032&lt;/span&gt;
    &lt;span class="n"&gt;n_ips&lt;/span&gt;          &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;      &lt;span class="mf"&gt;3.0&lt;/span&gt;  &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;typical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.028&lt;/span&gt;
&lt;span class="n"&gt;u0336&lt;/span&gt; &lt;span class="mi"&gt;09&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt; &lt;span class="mi"&gt;05&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&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;credential_stuffing&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.804&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;yes&lt;/span&gt;
    &lt;span class="n"&gt;new_device&lt;/span&gt;     &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;      &lt;span class="mf"&gt;1.0&lt;/span&gt;  &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;typical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.027&lt;/span&gt;
    &lt;span class="n"&gt;n_attempts&lt;/span&gt;     &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;      &lt;span class="mf"&gt;4.0&lt;/span&gt;  &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;typical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.019&lt;/span&gt;
    &lt;span class="n"&gt;n_fail&lt;/span&gt;         &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;      &lt;span class="mf"&gt;3.0&lt;/span&gt;  &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;typical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.017&lt;/span&gt;
&lt;span class="n"&gt;u0214&lt;/span&gt; &lt;span class="mi"&gt;09&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&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;normal&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;trip&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.766&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;yes&lt;/span&gt;
    &lt;span class="n"&gt;n_attempts&lt;/span&gt;     &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;      &lt;span class="mf"&gt;4.0&lt;/span&gt;  &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;typical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.085&lt;/span&gt;
    &lt;span class="n"&gt;geo_speed_kmh&lt;/span&gt;  &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="mf"&gt;25540.6&lt;/span&gt;  &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;typical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.048&lt;/span&gt;
    &lt;span class="n"&gt;n_ips&lt;/span&gt;          &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;      &lt;span class="mf"&gt;3.0&lt;/span&gt;  &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;typical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.039&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The impossible-travel alert reads well: new device plus 21,000 km/h. The stuffing alert points at the new device and the failures. Notice what’s missing, though: &lt;code&gt;ip_fanout&lt;/code&gt; never shows up, even though it’s the strongest stuffing signal. The third alert is a business trip, and its explanation shows the same impossible-travel pattern as a real attack, because a hotel IP on another continent &lt;em&gt;is&lt;/em&gt; geographically odd.&lt;/p&gt;

&lt;p&gt;Single-feature ablation has a known blind spot: when two features are odd together, resetting either one alone may barely move the score. In the test suite, a synthetic 30-failure window got its explanation attributed to &lt;code&gt;n_attempts&lt;/code&gt; rather than &lt;code&gt;n_fail&lt;/code&gt;, because both were extreme together. Treat explanations as hints for the analyst, not proofs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8. Drift and retraining
&lt;/h2&gt;

&lt;p&gt;Days 29-35 simulate a normal IT change week. Two things ship at once: a corporate VPN starts sending 40% of users through two gateways in Frankfurt, and a new SSO client silently re-authenticates half the users twice after every login, about 20 minutes apart. Days 29-30 have no attacks, so the model has a chance to re-baseline. Days 31-35 get a full set of attacks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PSI train -&amp;gt; IT-change week (&amp;gt;0.25 usually = significant shift):
    ip_fanout      2.64
    n_attempts     0.57
    hour_rarity    0.30
    new_asn        0.11

old model, old threshold, days 31-35: alerts=  367  precision=0.313  recall=0.943  PR-AUC=0.868
    alerts/day: 73  (budget was 20)
retrained (days 10-30), new threshold:   alerts=  269  precision=0.416  recall=0.918  PR-AUC=0.898
IPs with &amp;gt;50 distinct users in the IT-change week: ['198.51.100.10', '198.51.100.11']
old model + egress allowlist feature:   alerts=  356  precision=0.323  recall=0.943  PR-AUC=0.870

attack windows caught in days 31-35, by type:
                     old  retrained  allowlist  total
label                                                
brute_force            7          7          7      7
credential_stuffing   80         80         80     80
impossible_travel     10         10         10     10
low_and_slow          18         15         18     25

alerts per day, old model: [77, 33, 26, 28, 8, 51, 35, 87, 66, 74, 82, 30, 36, 145]
attack windows per day:    [52, 5, 3, 7, 4, 44, 8, 0, 0, 6, 6, 13, 24, 73]
false positives/day: test week 21.1 | days 31-35 old 50.4 | retrained 31.4 | allowlist 48.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Population Stability Index screams about &lt;code&gt;ip_fanout&lt;/code&gt; (2.64): two IPs now serve over 50 users each. A common rule of thumb treats PSI above 0.25 as a significant shift, though Yurdakul’s 2018 thesis notes these cut-offs are conventions with no stated error rates. But the feature with the loudest PSI isn’t what broke the alerts. Fixing it with an egress allowlist barely helps (50.4 → 48.2 false positives/day). My reading: each split picks one of ten features at random, so one extreme feature moves the average path length less than you might expect. The real damage comes from &lt;code&gt;n_attempts&lt;/code&gt;: background SSO re-auth makes a normal hour look like a busy hour, and “busy hour” was already the forest’s main false-positive pattern.&lt;/p&gt;

&lt;p&gt;Retraining on a rolling 21-day window (days 10-30, so it includes two days of the new normal) brings false positives down to 31.4/day and PR-AUC up from 0.868 to 0.898. It also costs something: it catches 15 low-and-slow attempts instead of 18. That’s the trade-off with every unsupervised retrain. The model learns whatever the window contains, including attacks nobody labeled.&lt;/p&gt;

&lt;p&gt;Two smaller findings. &lt;code&gt;hour_rarity&lt;/code&gt; drifts on its own (PSI 0.30) because per-user hour histograms keep sharpening as history grows: a cold-start effect, not a behavior change. And the 77 alerts on day 22 aren’t a model problem: that day contains a full stuffing campaign (52 attack windows). An alerts-per-day chart without attack counts next to it would have sent me looking for a bug.&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%2Fjjmbw5ww2txdhgdbzct1.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%2Fjjmbw5ww2txdhgdbzct1.png" alt=" " width="800" height="480"&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/media%2Fimage3.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/media%2Fimage3.png" alt="Alerts per day over two weeks, with the IT change marked: false positives per day go from 21.1 in the test week to 50.4 after the change, and 31.4 after retraining" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Alerts per day over two weeks, with the IT change marked: false positives per day go from 21.1 in the test week to 50.4 after the change, and 31.4 after retraining&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the model gets it wrong
&lt;/h2&gt;

&lt;p&gt;Collecting the failures in one place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Low-and-slow stuffing:&lt;/strong&gt; 12 of 25 caught. One failed login from the right city on the right device during working hours is indistinguishable from a typo. Only cross-account features (many accounts, same password-failure pattern, same hour) can see it, and that’s a different model.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Busy users:&lt;/strong&gt; most false positives are legitimate heavy users who mistyped a password.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Travelers:&lt;/strong&gt; a business trip through a foreign hotel IP looks exactly like impossible travel.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The victim’s return:&lt;/strong&gt; after a takeover, the real user’s next login looks like an anomaly. That’s useful, but it breaks naive labeling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Silent masking:&lt;/strong&gt; 20 attack windows sit in the training data. The paper calls this masking: a dense cluster of anomalies is harder to isolate. Real logs will have more of it than my 20.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gotchas I hit
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Window-boundary bugs in features.&lt;/strong&gt; The geo-velocity bug above raised no error, and PR-AUC only moved from 0.870 to 0.873 after the fix, because &lt;code&gt;new_device&lt;/code&gt; and &lt;code&gt;new_country&lt;/code&gt; carried those windows anyway. Only the explanation exposed it. Write a unit test for a two-logins-in-one-hour case.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;log1p&lt;/code&gt; &lt;strong&gt;didn’t matter here.&lt;/strong&gt; Isolation Forest splits uniformly between min and max, so I expected heavy tails to hurt. PR-AUC with log transforms was 0.873; with raw counts, 0.870. Measure before you add preprocessing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The “obvious” network feature didn’t help the score.&lt;/strong&gt; Dropping &lt;code&gt;ip_fanout&lt;/code&gt; moved PR-AUC from 0.873 to 0.875. The stuffing windows were already isolated by &lt;code&gt;new_device&lt;/code&gt; and failures. For a single strong signal, a rule is still a better tool.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Random seeds move PR-AUC by about 0.01.&lt;/strong&gt; Five &lt;code&gt;random_state&lt;/code&gt; values gave 0.866 to 0.878. Don’t celebrate a 0.005 improvement from a feature change without checking several seeds.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PR-AUC with log1p on counts: 0.873   raw counts: 0.870
PR-AUC over 5 random_state values: 0.878 0.869 0.871 0.870 0.870
PR-AUC without the network feature ip_fanout: 0.875
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tests that pin the behavior
&lt;/h2&gt;

&lt;p&gt;The suite checks the claims this post depends on: deterministic generation, causal features, failed attempts not whitelisting the attacker’s device, in-window impossible travel, &lt;code&gt;offset_&lt;/code&gt; equal to a training quantile, and explanations pointing at the right feature family.&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;test_features_are_causal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Appending future events must not change features of earlier windows.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;cut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&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;w_full&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="nf"&gt;build_windows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w_past&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="nf"&gt;build_windows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;early&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w_full&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;w_full&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;reset_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drop&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;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;FEATURES&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip_fanout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# fan-out is per hour, also causal
&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;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_frame_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;early&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;w_past&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cols&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;test_contamination_is_just_a_train_quantile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;w&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="nf"&gt;build_windows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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;to_matrix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&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;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contamination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;offset_&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score_samples&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;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;m&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="o"&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="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.002&lt;/span&gt;
&lt;span class="p"&gt;.......&lt;/span&gt;                                                                  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="mf"&gt;2.98&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Production checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Compute features &lt;strong&gt;causally&lt;/strong&gt;, from state that only successful logins update, and test window boundaries explicitly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set the threshold from an &lt;strong&gt;alert budget&lt;/strong&gt; on recent data, not from &lt;code&gt;contamination="auto"&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Report &lt;strong&gt;precision, recall and PR-AUC per attack type&lt;/strong&gt;, plus false positives per day. A single F1 hides the low-and-slow row.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep &lt;strong&gt;rules for known high-signal patterns&lt;/strong&gt; (fan-out, failure bursts, impossible travel) and use the forest as a ranked review queue for the rest.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ship an &lt;strong&gt;explanation&lt;/strong&gt; with every alert, and treat it as a hint.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Monitor &lt;strong&gt;false positives per day&lt;/strong&gt;, not only PSI. The loudest drifting feature may not be the one that hurts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retrain on a &lt;strong&gt;rolling window&lt;/strong&gt; after known changes, compare recall per attack type before and after, and allowlist infrastructure such as VPN egress IPs in the features.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Treat user feedback (“that was me, I was traveling”) as labels, and eventually move to a supervised model on top of these features.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Isolation Forest is a reasonable first anomaly detection algorithm for login logs: it needs no labels, trains in seconds and gives a ranked list. On this data it didn’t replace rules. It complemented them, catching half of an attack class the rules missed, at the cost of a noisier queue. Most of the work, and most of the bugs, were in the features and the threshold, not the model.&lt;/p&gt;

&lt;p&gt;What does your team use for suspicious-login detection: vendor risk scores, hand-written SIEM rules, or your own models? And how do you get labels for the alerts nobody escalates?&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>security</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Building a Web MVP That Does Not Collapse After the First 100 Users</title>
      <dc:creator>Kate Steele</dc:creator>
      <pubDate>Tue, 20 Oct 2026 14:16:52 +0000</pubDate>
      <link>https://dev.to/kate_steeleeee/building-a-web-mvp-that-does-not-collapse-after-the-first-100-users-2d41</link>
      <guid>https://dev.to/kate_steeleeee/building-a-web-mvp-that-does-not-collapse-after-the-first-100-users-2d41</guid>
      <description>&lt;p&gt;Most MVP architecture advice falls into one of two extremes.&lt;br&gt;
The first says, “Move fast, because everything will be rewritten.” The result is often a fragile application with business logic inside UI components, inconsistent data, missing logs, and production changes that feel dangerous.&lt;br&gt;
The second says, “Design for millions of users from day one.” That path produces infrastructure for traffic the product has not earned and a delivery schedule that postpones the most important test: whether anybody needs the product.&lt;br&gt;
A better web MVP sits between those extremes. It is intentionally small, technically legible, and operationally safe. It does not predict every future requirement, but it gives the team room to learn without rebuilding after each discovery.&lt;br&gt;
Here is a practical way to approach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the product boundary before the system boundary
&lt;/h2&gt;

&lt;p&gt;Architecture starts with the user outcome.&lt;br&gt;
Write down the primary actor, the action they need to complete, and the state change that proves the action succeeded. For an appointment platform, the important state change may be “available slot” to “confirmed booking.” For a B2B reporting tool, it may be “raw data uploaded” to “report shared.”&lt;br&gt;
This definition prevents the technical design from expanding around hypothetical features. It also helps identify the minimum set of capabilities required across the stack.&lt;br&gt;
Map the happy path first, then add the failure paths that would damage trust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invalid or incomplete input;&lt;/li&gt;
&lt;li&gt;expired sessions;&lt;/li&gt;
&lt;li&gt;rejected payments;&lt;/li&gt;
&lt;li&gt;duplicate requests;&lt;/li&gt;
&lt;li&gt;unavailable third-party services;&lt;/li&gt;
&lt;li&gt;authorization failures;&lt;/li&gt;
&lt;li&gt;interrupted uploads;&lt;/li&gt;
&lt;li&gt;notifications that cannot be delivered.
An MVP can omit secondary workflows. It cannot pretend that predictable failures do not exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Prefer a modular monolith until distribution solves a real problem
&lt;/h2&gt;

&lt;p&gt;Microservices are useful when teams need independent deployment, scaling, ownership, or fault isolation. Most early products do not yet have those constraints.&lt;br&gt;
A modular monolith is often a stronger starting point. Keep clear internal boundaries for accounts, billing, inventory, messaging, reporting, or other domains. Modules should expose deliberate interfaces rather than reaching into one another’s tables and private logic.&lt;br&gt;
This structure keeps development and deployment understandable. It also allows later extraction if one domain develops different performance, security, or ownership needs.&lt;br&gt;
The important word is modular. A single codebase is not automatically simple. Naming, folder structure, dependency direction, and ownership of business rules still matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Keep business rules out of the browser
&lt;/h2&gt;

&lt;p&gt;Modern front-end frameworks make it easy to place substantial logic in client components. Some validation belongs there because immediate feedback improves usability. The source of truth should still live on the server.&lt;br&gt;
Prices, permissions, eligibility, quotas, workflow transitions, and transaction status should be verified by trusted backend code. A user can modify requests, replay calls, or bypass interface restrictions. The application must remain correct when the browser behaves unexpectedly.&lt;br&gt;
A useful test is to imagine replacing the current interface with a mobile client or partner API. If the core rules would need to be rebuilt, too much domain logic probably lives in the front end.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Model data around identities and state transitions
&lt;/h2&gt;

&lt;p&gt;Early schemas often mirror screens. That works until the product adds another role, a review process, historical reporting, or an integration with a different data lifecycle.&lt;br&gt;
Define the main entities, ownership rules, and transitions. Ask which records represent durable facts, which changes need a history, who may modify each record, what must remain unique, and what happens when related data is deleted.&lt;br&gt;
Use database constraints for facts the database can enforce. A unique email, one active reservation per slot, or a required relationship should not rely only on application code.&lt;br&gt;
Migrations should be versioned from the beginning. “We will clean up the database after validation” is rarely as easy as it sounds because production data quickly becomes part of the product’s reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Buy commodity capabilities, own differentiating logic
&lt;/h2&gt;

&lt;p&gt;A first release usually should not include a custom identity system, email delivery service, payment processor, file storage layer, or analytics engine. Mature providers can remove months of non-differentiating work.&lt;br&gt;
Outsourcing a capability does not outsource responsibility. Wrap third-party APIs behind your own service boundary. Keep provider-specific identifiers separate from internal IDs. Verify webhook signatures. Handle retries and duplicated events. Store enough context to investigate failures without logging secrets.&lt;br&gt;
Also define what happens when a dependency is slow or unavailable. Can the user retry safely? Does the system display a pending state? Will a background job reconcile the result? Is an operator alerted?&lt;br&gt;
Integrations are part of the application, not plumbing outside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Implement authorization as a policy, not a scattered condition
&lt;/h2&gt;

&lt;p&gt;Authentication answers who the user is. Authorization answers what that user may do.&lt;br&gt;
Even a simple MVP can have customers, operators, administrators, owners, and invited members. Rules spread quickly when every controller contains its own role check.&lt;br&gt;
Centralize authorization decisions and check both action and resource. Include negative tests that verify users cannot read, update, or infer resources outside their scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Add observability before you need to debug a customer’s problem
&lt;/h2&gt;

&lt;p&gt;A console full of unstructured messages is not an operational plan.&lt;br&gt;
At minimum, production should provide searchable application logs, error reporting with stack traces, health checks, basic performance metrics, and alerts for failures that require action. Use correlation or request IDs so one user journey can be followed across application code and external calls.&lt;br&gt;
Logs should answer practical questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which user or organization experienced the problem?&lt;/li&gt;
&lt;li&gt;What operation was attempted?&lt;/li&gt;
&lt;li&gt;Which application version handled it?&lt;/li&gt;
&lt;li&gt;Which external provider was involved?&lt;/li&gt;
&lt;li&gt;Was the request retried?&lt;/li&gt;
&lt;li&gt;Did the final state become success, failure, or pending?
Do not place passwords, tokens, full payment data, or other sensitive fields in logs. Observability should improve control without creating another security problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Build responsive behavior into the component system
&lt;/h2&gt;

&lt;p&gt;“Mobile support later” often means redesign later.&lt;br&gt;
Responsive web development is not only about adding breakpoints. It affects information hierarchy, table behavior, navigation, touch targets, form length, upload flows, modal usage, and the amount of context visible at once.&lt;br&gt;
Define a small set of layout primitives and test the core journey on real viewport sizes as it is built. Do not wait until the final QA week to discover that a critical dashboard becomes unusable on a narrow screen.&lt;br&gt;
The first release may optimize for desktop if the audience works primarily on large displays. It should still fail gracefully on other devices and communicate unsupported constraints clearly when necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Protect the deployment path
&lt;/h2&gt;

&lt;p&gt;An MVP needs a repeatable route from code to production.&lt;br&gt;
Use separate test and production environments. Keep configuration outside the codebase. Automate checks for the most valuable workflows, include migrations in the release procedure, maintain a rollback or forward-fix strategy, and test data recovery.&lt;br&gt;
The pipeline does not need dozens of stages. It needs to make routine changes boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Optimize what users can feel
&lt;/h2&gt;

&lt;p&gt;Premature optimization wastes time, but ignoring performance distorts product feedback.&lt;br&gt;
Measure page load, API latency, slow queries, and front-end weight. Compress images, paginate large results, cache stable content, and avoid sending data the current view does not need. Focus on the main journey, not an impressive landing-page score that hides a slow signed-in workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review architecture through the MVP question
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://pixelplex.io/services/mvp-web-development-company/" rel="noopener noreferrer"&gt;web development MVP&lt;/a&gt; should make the product easier to test, operate, and change. Every technical choice can be reviewed against those three goals.&lt;br&gt;
Does the choice help the team release a credible user outcome? Can developers and operators understand failures? Can the system absorb the next likely change without a rewrite? Is the complexity solving a present constraint or only an imagined future one?&lt;br&gt;
The right architecture is not the most advanced architecture. It is the smallest structure that protects learning.&lt;br&gt;
Build the narrow path well. Keep rules explicit, data consistent, integrations observable, permissions enforceable, and releases repeatable. Then let actual usage tell you which part of the system deserves to become more sophisticated.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>mvp</category>
      <category>startup</category>
    </item>
    <item>
      <title>Building a Web MVP That Does Not Collapse After the First 100 Users</title>
      <dc:creator>Kate Steele</dc:creator>
      <pubDate>Fri, 16 Oct 2026 15:03:30 +0000</pubDate>
      <link>https://dev.to/kate_steeleeee/building-a-web-mvp-that-does-not-collapse-after-the-first-100-users-4p0</link>
      <guid>https://dev.to/kate_steeleeee/building-a-web-mvp-that-does-not-collapse-after-the-first-100-users-4p0</guid>
      <description>&lt;p&gt;Most MVP architecture advice falls into one of two extremes.&lt;br&gt;
The first says, “Move fast, because everything will be rewritten.” The result is often a fragile application with business logic inside UI components, inconsistent data, missing logs, and production changes that feel dangerous.&lt;br&gt;
The second says, “Design for millions of users from day one.” That path produces infrastructure for traffic the product has not earned and a delivery schedule that postpones the most important test: whether anybody needs the product.&lt;br&gt;
A better web MVP sits between those extremes. It is intentionally small, technically legible, and operationally safe. It does not predict every future requirement, but it gives the team room to learn without rebuilding after each discovery.&lt;br&gt;
Here is a practical way to approach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the product boundary before the system boundary
&lt;/h2&gt;

&lt;p&gt;Architecture starts with the user outcome.&lt;br&gt;
Write down the primary actor, the action they need to complete, and the state change that proves the action succeeded. For an appointment platform, the important state change may be “available slot” to “confirmed booking.” For a B2B reporting tool, it may be “raw data uploaded” to “report shared.”&lt;br&gt;
This definition prevents the technical design from expanding around hypothetical features. It also helps identify the minimum set of capabilities required across the stack.&lt;br&gt;
Map the happy path first, then add the failure paths that would damage trust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invalid or incomplete input;&lt;/li&gt;
&lt;li&gt;expired sessions;&lt;/li&gt;
&lt;li&gt;rejected payments;&lt;/li&gt;
&lt;li&gt;duplicate requests;&lt;/li&gt;
&lt;li&gt;unavailable third-party services;&lt;/li&gt;
&lt;li&gt;authorization failures;&lt;/li&gt;
&lt;li&gt;interrupted uploads;&lt;/li&gt;
&lt;li&gt;notifications that cannot be delivered.
An MVP can omit secondary workflows. It cannot pretend that predictable failures do not exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Prefer a modular monolith until distribution solves a real problem
&lt;/h2&gt;

&lt;p&gt;Microservices are useful when teams need independent deployment, scaling, ownership, or fault isolation. Most early products do not yet have those constraints.&lt;br&gt;
A modular monolith is often a stronger starting point. Keep clear internal boundaries for accounts, billing, inventory, messaging, reporting, or other domains. Modules should expose deliberate interfaces rather than reaching into one another’s tables and private logic.&lt;br&gt;
This structure keeps development and deployment understandable. It also allows later extraction if one domain develops different performance, security, or ownership needs.&lt;br&gt;
The important word is modular. A single codebase is not automatically simple. Naming, folder structure, dependency direction, and ownership of business rules still matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Keep business rules out of the browser
&lt;/h2&gt;

&lt;p&gt;Modern front-end frameworks make it easy to place substantial logic in client components. Some validation belongs there because immediate feedback improves usability. The source of truth should still live on the server.&lt;br&gt;
Prices, permissions, eligibility, quotas, workflow transitions, and transaction status should be verified by trusted backend code. A user can modify requests, replay calls, or bypass interface restrictions. The application must remain correct when the browser behaves unexpectedly.&lt;br&gt;
A useful test is to imagine replacing the current interface with a mobile client or partner API. If the core rules would need to be rebuilt, too much domain logic probably lives in the front end.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Model data around identities and state transitions
&lt;/h2&gt;

&lt;p&gt;Early schemas often mirror screens. That works until the product adds another role, a review process, historical reporting, or an integration with a different data lifecycle.&lt;br&gt;
Define the main entities, ownership rules, and transitions. Ask which records represent durable facts, which changes need a history, who may modify each record, what must remain unique, and what happens when related data is deleted.&lt;br&gt;
Use database constraints for facts the database can enforce. A unique email, one active reservation per slot, or a required relationship should not rely only on application code.&lt;br&gt;
Migrations should be versioned from the beginning. “We will clean up the database after validation” is rarely as easy as it sounds because production data quickly becomes part of the product’s reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Buy commodity capabilities, own differentiating logic
&lt;/h2&gt;

&lt;p&gt;A first release usually should not include a custom identity system, email delivery service, payment processor, file storage layer, or analytics engine. Mature providers can remove months of non-differentiating work.&lt;br&gt;
Outsourcing a capability does not outsource responsibility. Wrap third-party APIs behind your own service boundary. Keep provider-specific identifiers separate from internal IDs. Verify webhook signatures. Handle retries and duplicated events. Store enough context to investigate failures without logging secrets.&lt;br&gt;
Also define what happens when a dependency is slow or unavailable. Can the user retry safely? Does the system display a pending state? Will a background job reconcile the result? Is an operator alerted?&lt;br&gt;
Integrations are part of the application, not plumbing outside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Implement authorization as a policy, not a scattered condition
&lt;/h2&gt;

&lt;p&gt;Authentication answers who the user is. Authorization answers what that user may do.&lt;br&gt;
Even a simple MVP can have customers, operators, administrators, owners, and invited members. Rules spread quickly when every controller contains its own role check.&lt;br&gt;
Centralize authorization decisions and check both action and resource. Include negative tests that verify users cannot read, update, or infer resources outside their scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Add observability before you need to debug a customer’s problem
&lt;/h2&gt;

&lt;p&gt;A console full of unstructured messages is not an operational plan.&lt;br&gt;
At minimum, production should provide searchable application logs, error reporting with stack traces, health checks, basic performance metrics, and alerts for failures that require action. Use correlation or request IDs so one user journey can be followed across application code and external calls.&lt;br&gt;
Logs should answer practical questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which user or organization experienced the problem?&lt;/li&gt;
&lt;li&gt;What operation was attempted?&lt;/li&gt;
&lt;li&gt;Which application version handled it?&lt;/li&gt;
&lt;li&gt;Which external provider was involved?&lt;/li&gt;
&lt;li&gt;Was the request retried?&lt;/li&gt;
&lt;li&gt;Did the final state become success, failure, or pending?
Do not place passwords, tokens, full payment data, or other sensitive fields in logs. Observability should improve control without creating another security problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Build responsive behavior into the component system
&lt;/h2&gt;

&lt;p&gt;“Mobile support later” often means redesign later.&lt;br&gt;
Responsive web development is not only about adding breakpoints. It affects information hierarchy, table behavior, navigation, touch targets, form length, upload flows, modal usage, and the amount of context visible at once.&lt;br&gt;
Define a small set of layout primitives and test the core journey on real viewport sizes as it is built. Do not wait until the final QA week to discover that a critical dashboard becomes unusable on a narrow screen.&lt;br&gt;
The first release may optimize for desktop if the audience works primarily on large displays. It should still fail gracefully on other devices and communicate unsupported constraints clearly when necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Protect the deployment path
&lt;/h2&gt;

&lt;p&gt;An MVP needs a repeatable route from code to production.&lt;br&gt;
Use separate test and production environments. Keep configuration outside the codebase. Automate checks for the most valuable workflows, include migrations in the release procedure, maintain a rollback or forward-fix strategy, and test data recovery.&lt;br&gt;
The pipeline does not need dozens of stages. It needs to make routine changes boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Optimize what users can feel
&lt;/h2&gt;

&lt;p&gt;Premature optimization wastes time, but ignoring performance distorts product feedback.&lt;br&gt;
Measure page load, API latency, slow queries, and front-end weight. Compress images, paginate large results, cache stable content, and avoid sending data the current view does not need. Focus on the main journey, not an impressive landing-page score that hides a slow signed-in workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review architecture through the MVP question
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://pixelplex.io/services/mvp-web-development-company/" rel="noopener noreferrer"&gt;web development MVP&lt;/a&gt; should make the product easier to test, operate, and change. Every technical choice can be reviewed against those three goals.&lt;br&gt;
Does the choice help the team release a credible user outcome? Can developers and operators understand failures? Can the system absorb the next likely change without a rewrite? Is the complexity solving a present constraint or only an imagined future one?&lt;br&gt;
The right architecture is not the most advanced architecture. It is the smallest structure that protects learning.&lt;br&gt;
Build the narrow path well. Keep rules explicit, data consistent, integrations observable, permissions enforceable, and releases repeatable. Then let actual usage tell you which part of the system deserves to become more sophisticated.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building a Production-Ready Stellar Payment App Beyond sendPayment()</title>
      <dc:creator>Kate Steele</dc:creator>
      <pubDate>Tue, 06 Oct 2026 08:32:45 +0000</pubDate>
      <link>https://dev.to/kate_steeleeee/building-a-production-ready-stellar-payment-app-beyond-sendpayment-38l1</link>
      <guid>https://dev.to/kate_steeleeee/building-a-production-ready-stellar-payment-app-beyond-sendpayment-38l1</guid>
      <description>&lt;p&gt;A basic Stellar payment demo can be built quickly. Load an account, create a transaction, add a payment operation, sign it, submit it, and display the hash.&lt;/p&gt;

&lt;p&gt;A production application must also prevent duplicate transfers, coordinate sequence numbers, support multiple assets, connect to traditional rails, explain pending states, and reconcile every financial event.&lt;/p&gt;

&lt;p&gt;The network transaction is one step in a larger workflow. This article outlines the architecture around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the account and custody model first
&lt;/h2&gt;

&lt;p&gt;A separate Stellar account for each user provides a distinct on-chain identity and observable balances. It also requires account creation, reserve management, trustlines for issued assets, and a custody model.&lt;/p&gt;

&lt;p&gt;A pooled account represents customers in an internal ledger and can differentiate them through mixed accounts or memos. It reduces network account overhead but increases reliance on internal accounting and controls.&lt;/p&gt;

&lt;p&gt;A non-custodial wallet lets the user sign. A custodial product signs on the customer’s behalf and therefore needs hardened key management, withdrawal controls, approval policies, and monitoring.&lt;/p&gt;

&lt;p&gt;Before implementation, answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who can authorize a transfer?&lt;/li&gt;
&lt;li&gt;Where do signing keys live?&lt;/li&gt;
&lt;li&gt;Who pays network fees and reserves?&lt;/li&gt;
&lt;li&gt;How is an on-chain account linked to a customer?&lt;/li&gt;
&lt;li&gt;What happens when credentials are lost?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These decisions cannot be hidden behind a wallet library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep an internal payment record
&lt;/h2&gt;

&lt;p&gt;Never use a transaction hash as the only application record.&lt;br&gt;
Create a payment entity before submitting anything:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;payment_id&lt;br&gt;
idempotency_key&lt;br&gt;
customer_id&lt;br&gt;
source_account&lt;br&gt;
destination_account&lt;br&gt;
send_asset&lt;br&gt;
send_amount&lt;br&gt;
receive_asset&lt;br&gt;
minimum_receive_amount&lt;br&gt;
quote_id&lt;br&gt;
stellar_transaction_hash&lt;br&gt;
status&lt;br&gt;
failure_code&lt;/code&gt;&lt;br&gt;
The application owns the request and business state. Stellar provides the authoritative network result.&lt;/p&gt;

&lt;p&gt;A useful state machine may include:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CREATED&lt;br&gt;
AWAITING_SIGNATURE&lt;br&gt;
READY_TO_SUBMIT&lt;br&gt;
SUBMITTED&lt;br&gt;
CONFIRMED&lt;br&gt;
PAYOUT_PENDING&lt;br&gt;
COMPLETED&lt;br&gt;
FAILED&lt;br&gt;
REFUND_REQUIRED&lt;br&gt;
REFUNDED&lt;/code&gt;&lt;br&gt;
Transitions should be explicit. The same idempotency key must return the existing payment. After an uncertain HTTP response, query stored state and the network before building another transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manage sequence numbers under concurrency
&lt;/h2&gt;

&lt;p&gt;Every Stellar transaction uses a source account sequence number. Two workers that load the same account simultaneously can prepare transactions with the same next sequence. One succeeds and the other fails.&lt;/p&gt;

&lt;p&gt;Serialize submissions per source account with a queue or lock. At higher rates, channel accounts can distribute transaction sources where the signing model permits.&lt;/p&gt;

&lt;p&gt;Do not trust an in-memory counter. Restarts, timeouts, or external submissions can invalidate it. Reconcile sequence state whenever the result is uncertain.&lt;/p&gt;

&lt;p&gt;Add time bounds so a signed transaction cannot remain valid indefinitely. Stellar sequence numbers are consumed by transactions and help enforce ordered, one-time transaction execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build transactions from validated intent
&lt;/h2&gt;

&lt;p&gt;The backend should accept a business command such as “send 50 units of asset A to recipient B,” not an arbitrary serialized transaction from an untrusted client.&lt;/p&gt;

&lt;p&gt;Validate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source authorization;&lt;/li&gt;
&lt;li&gt;destination format;&lt;/li&gt;
&lt;li&gt;asset code and issuer;&lt;/li&gt;
&lt;li&gt;required trustline;&lt;/li&gt;
&lt;li&gt;available balance;&lt;/li&gt;
&lt;li&gt;transaction limits;&lt;/li&gt;
&lt;li&gt;quote expiration;&lt;/li&gt;
&lt;li&gt;compliance status;&lt;/li&gt;
&lt;li&gt;fee and memo requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build the transaction deterministically and show the destination, asset, amount, fees, and conversion limits before signing.&lt;/p&gt;

&lt;p&gt;When a client constructs the transaction, the signer should still decode and verify every operation instead of approving opaque XDR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle issued assets deliberately
&lt;/h2&gt;

&lt;p&gt;XLM does not require a trustline, while classic issued assets generally do. A recipient without the appropriate trustline cannot receive the asset through a normal payment.&lt;/p&gt;

&lt;p&gt;Check this before submission. A wallet can guide trustline creation, while a custodial service may create or sponsor required entries.&lt;/p&gt;

&lt;p&gt;Asset identity is the combination of code and issuer, not the code alone. Store and display both. Maintain an allowlist when the product supports a controlled asset catalog.&lt;/p&gt;

&lt;p&gt;Authorization-required, revocable, and clawback-enabled assets have different risk and support implications. Make relevant restrictions visible. Stellar assets can also interact with Soroban contracts through the built-in Stellar Asset Contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Match path payments to the quote
&lt;/h2&gt;

&lt;p&gt;Path payments allow the source and destination assets to differ.&lt;/p&gt;

&lt;p&gt;Choose the operation according to the customer promise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;_PathPaymentStrictSend _fixes the source amount and sets a minimum destination amount.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;PathPaymentStrictReceive&lt;/em&gt; fixes the destination amount and sets a maximum source amount.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Query paths before building the transaction, then apply limits that match the quoted rate and expiration. Never silently widen the tolerance after approval.&lt;/p&gt;

&lt;p&gt;A quote record should preserve the source amount, destination amount, fees, expiry, and route assumptions. When it expires, return to pricing rather than submitting under new economics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrate anchors as durable workflows
&lt;/h2&gt;

&lt;p&gt;Anchors connect Stellar assets with off-chain payment rails. Common Stellar Ecosystem Proposals cover service discovery, authentication, customer information, deposits, withdrawals, cross-border payments, and quotes.&lt;/p&gt;

&lt;p&gt;A typical integration may use SEP-1, SEP-10, SEP-12, SEP-6 or SEP-24, SEP-31, and SEP-38.&lt;/p&gt;

&lt;p&gt;Use an anchor adapter and persist the provider transaction ID, customer reference, quote, rail, required actions, and status history.&lt;/p&gt;

&lt;p&gt;SEP-24 commonly gives the anchor control of an interactive deposit or withdrawal flow. SEP-6 supports a more programmatic experience. The choice affects UX and compliance responsibilities.&lt;/p&gt;

&lt;p&gt;Callbacks and polling results may arrive more than once or out of order. Updates must be idempotent, and provider-specific evidence should be retained for support and reconciliation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ingest network results independently
&lt;/h2&gt;

&lt;p&gt;The transaction submitter should not be the only component that knows whether a transfer succeeded.&lt;/p&gt;

&lt;p&gt;Run an ingestion process that follows relevant accounts, operations, or contract events from a durable cursor. Convert network records into internal events such as:&lt;br&gt;
&lt;code&gt;TransactionConfirmed&lt;br&gt;
IncomingPaymentObserved&lt;br&gt;
OutgoingPaymentObserved&lt;br&gt;
TrustlineChanged&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Store the hash, ledger, operation index, asset, amount, source, destination, memo, and cursor. Deduplicate by stable network identifiers.&lt;/p&gt;

&lt;p&gt;Independent ingestion catches external submissions and cases in which Stellar accepts a transaction but the client times out.&lt;/p&gt;

&lt;p&gt;Advance the cursor only after the related database changes are committed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate confirmation from business completion
&lt;/h2&gt;

&lt;p&gt;A confirmed Stellar transaction may not mean the customer journey is finished.&lt;/p&gt;

&lt;p&gt;For a wallet transfer, confirmation may be final. For a withdrawal, an anchor may still need to send funds through a bank or mobile-money rail. For a merchant payment, another service may need to release the order.&lt;/p&gt;

&lt;p&gt;Model these stages separately. A payment can be confirmed on Stellar while its payout remains pending. This prevents premature notifications and makes support investigations clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sponsor fees with policy controls
&lt;/h2&gt;

&lt;p&gt;Fee-bump transactions allow one account to pay the fee for another account’s transaction without changing the inner transaction’s authorization. Sponsored reserves can help cover selected ledger-entry requirements.&lt;/p&gt;

&lt;p&gt;Before the sponsor signs, validate the inner transaction, customer, asset, amount, and allowed operations. Add rate limits and abuse monitoring. Sponsorship must not become an arbitrary transaction relay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile every view of value
&lt;/h2&gt;

&lt;p&gt;A custodial or hybrid product usually has three views:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the application’s customer ledger;&lt;/li&gt;
&lt;li&gt;balances and transactions on Stellar;&lt;/li&gt;
&lt;li&gt;balances and statuses held by anchor, banking, or payment partners.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compare them continuously and classify differences as expected delay, missing callback, duplicate event, failed payout, unmatched deposit, or manual adjustment.&lt;/p&gt;

&lt;p&gt;Operator tools should search by internal payment ID, network hash, account, memo, mixed ID, anchor transaction, and bank reference. Corrections need approvals and audit history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test uncertainty, not only valid transfers
&lt;/h2&gt;

&lt;p&gt;Cover duplicate API requests, concurrent submissions, expired time bounds, missing trustlines, stale paths, rejected signatures, submission timeouts, duplicate anchor updates, cursor restarts, and payout failure after on-chain settlement.&lt;/p&gt;

&lt;p&gt;Use Testnet for integration behavior, but keep deterministic unit tests for construction and state transitions. External service changes should not make the entire suite unreliable.&lt;/p&gt;

&lt;p&gt;Expert &lt;a href="https://pixelplex.io/services/stellar-blockchain-development-company/" rel="noopener noreferrer"&gt;Stellar blockchain development services&lt;/a&gt; should therefore cover more than SDK integration. The critical work lies in account architecture, custody, orchestration, anchor connectivity, reconciliation, observability, and safe operations.&lt;/p&gt;

&lt;p&gt;A payment app becomes production-ready when every transfer has one understandable history. Customers see clear status, support can explain events, finance can reconcile value, and engineers can retry safe steps without duplication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sendPayment()&lt;/strong&gt; is the visible action. The system around it is the product.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>architecture</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Single-Chain or Cross-Chain DEX? A Product Decision Guide</title>
      <dc:creator>Kate Steele</dc:creator>
      <pubDate>Thu, 17 Sep 2026 09:28:22 +0000</pubDate>
      <link>https://dev.to/kate_steeleeee/single-chain-or-cross-chain-dex-a-product-decision-guide-17hn</link>
      <guid>https://dev.to/kate_steeleeee/single-chain-or-cross-chain-dex-a-product-decision-guide-17hn</guid>
      <description>&lt;p&gt;A new decentralized exchange often begins with an ambitious idea: let users trade assets from many blockchain networks in one place. The promise is appealing. A broader asset range can attract more communities and reduce the need to move between separate applications.&lt;/p&gt;

&lt;p&gt;Cross-chain access, however, is not only an expansion of a single-chain product. It changes the user journey, liquidity strategy, security model, support process, and operating responsibilities.&lt;/p&gt;

&lt;p&gt;For some teams, a multi-chain or cross-chain DEX is the right destination. For others, starting with one ecosystem creates a better product and a safer path to growth. The decision should be based on a defined user problem rather than the assumption that more networks automatically mean more value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the user’s actual obstacle
&lt;/h2&gt;

&lt;p&gt;The first question is not “How many chains should we support?” It is “What prevents the target user from completing the desired trade today?”&lt;/p&gt;

&lt;p&gt;Users may face different obstacles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The assets they need exist on separate networks.&lt;/li&gt;
&lt;li&gt;Liquidity is fragmented across several venues.&lt;/li&gt;
&lt;li&gt;Moving assets requires too many unfamiliar steps.&lt;/li&gt;
&lt;li&gt;The preferred network has limited token variety.&lt;/li&gt;
&lt;li&gt;Existing tools do not explain routes and fees clearly.&lt;/li&gt;
&lt;li&gt;A community needs one interface for several deployments of the same protocol.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each obstacle suggests a different product.&lt;/p&gt;

&lt;p&gt;If users mainly trade assets within one ecosystem, a focused single-chain exchange may solve the problem. If they regularly move value between ecosystems, cross-chain functionality may be central. If the main issue is fragmented pricing, aggregation could matter more than native deployment on many networks.&lt;/p&gt;

&lt;p&gt;The scope should follow the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a single-chain launch can be a strength
&lt;/h2&gt;

&lt;p&gt;Supporting one network may look less ambitious, but it gives the team a smaller environment in which to build market quality.&lt;/p&gt;

&lt;p&gt;The product can focus its liquidity strategy on selected pairs, optimize wallet and transaction flows for one ecosystem, and create clearer support materials. Monitoring is also easier because the team works with one set of network conditions, token standards, and infrastructure dependencies.&lt;/p&gt;

&lt;p&gt;This focus can benefit users. They see fewer network switches, fewer asset versions, and fewer route variations. The interface can use language and defaults suited to the ecosystem rather than presenting a generic experience for everyone.&lt;/p&gt;

&lt;p&gt;A single-chain DEX can also develop a recognizable role, such as the preferred market for a specific community, application ecosystem, or category of assets. Depth in one market can be more valuable than weak coverage across many.&lt;/p&gt;

&lt;h2&gt;
  
  
  When multi-chain deployment makes sense
&lt;/h2&gt;

&lt;p&gt;A protocol may have active communities on several networks. In that case, deploying a similar exchange experience to each network can extend reach without necessarily creating direct cross-chain swaps.&lt;/p&gt;

&lt;p&gt;Users trade locally on the chain they have selected. Liquidity remains separate, but the product can provide a consistent interface, shared brand, and unified analytics.&lt;/p&gt;

&lt;p&gt;This model can work when each ecosystem has enough users and liquidity to support its own markets. It also allows the team to expand step by step. One deployment can be tested before the next is introduced.&lt;/p&gt;

&lt;p&gt;The limitation is fragmentation. The same asset pair may have different prices, liquidity levels, and incentives across networks. Users may still need another mechanism to move assets between them.&lt;/p&gt;

&lt;p&gt;The product should not present separate deployments as one unified market.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-chain trading adds another transaction story
&lt;/h2&gt;

&lt;p&gt;A standard swap already involves several concepts: token approval, quoted output, price impact, network fees, and confirmation. A cross-chain action can add source and destination networks, bridging, route providers, multiple fees, longer completion times, and more transaction states.&lt;/p&gt;

&lt;p&gt;The interface must turn that process into a story the user can follow.&lt;/p&gt;

&lt;p&gt;Before confirmation, the product should show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which asset leaves the source wallet;&lt;/li&gt;
&lt;li&gt;which asset is expected on the destination;&lt;/li&gt;
&lt;li&gt;the networks involved;&lt;/li&gt;
&lt;li&gt;the estimated total cost;&lt;/li&gt;
&lt;li&gt;the expected route and provider;&lt;/li&gt;
&lt;li&gt;important timing or liquidity conditions;&lt;/li&gt;
&lt;li&gt;what happens if one step cannot be completed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After confirmation, progress should remain visible. “Pending” is not enough when a transaction passes through several systems. Users need to know which step has completed and whether action is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Liquidity is not automatically unified
&lt;/h2&gt;

&lt;p&gt;Adding several networks does not create one deep market by itself. Liquidity may remain divided among pools, bridges, wrapped assets, and external venues.&lt;/p&gt;

&lt;p&gt;The routing layer must decide how to connect available sources. The best path may change according to trade size, fees, asset versions, network activity, and available depth. A route that works for a small swap may be unsuitable for a larger one.&lt;/p&gt;

&lt;p&gt;The exchange needs rules for evaluating routes, rejecting unreliable options, and explaining trade-offs. The lowest visible fee is not always the best choice if the route is slow or difficult to recover.&lt;/p&gt;

&lt;p&gt;Teams should measure execution quality across the complete route rather than evaluating only the swap on the destination network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asset identity becomes a product problem
&lt;/h2&gt;

&lt;p&gt;Multi-chain environments often contain native assets, bridged representations, and tokens with similar names or symbols. Users may not understand why two versions of an asset are not interchangeable.&lt;/p&gt;

&lt;p&gt;The DEX should provide clear asset metadata and show the network and token identity throughout the journey. Search results, balances, quotes, and transaction history should use consistent labels.&lt;/p&gt;

&lt;p&gt;The product also needs a policy for supported assets. Permissionless market creation can coexist with verified token information, but the distinction must be visible. Otherwise, broader access can increase confusion and support cases.&lt;/p&gt;

&lt;p&gt;Asset discovery is not a minor interface detail. It is part of transaction safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every added network expands operations
&lt;/h2&gt;

&lt;p&gt;A new chain introduces more than another option in a network selector. The team may need additional nodes or data providers, wallet integrations, token lists, monitoring rules, deployment processes, and support documentation.&lt;/p&gt;

&lt;p&gt;Network behavior also varies. Confirmation expectations, fee patterns, transaction formats, and infrastructure reliability may differ. The product must communicate these differences without making the experience feel inconsistent.&lt;/p&gt;

&lt;p&gt;Before adding a network, the team should define who owns its operations, how incidents will be detected, and what level of usage would justify continued support. An integration that exists but is rarely monitored can become a long-term liability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-chain dependencies need explicit risk boundaries
&lt;/h2&gt;

&lt;p&gt;Cross-chain products commonly depend on external bridges, messaging systems, route providers, or liquidity sources. These components may be operated and upgraded independently.&lt;/p&gt;

&lt;p&gt;The team needs to know how each dependency is monitored and what happens when it is unavailable. The interface should also identify when another protocol handles part of the route.&lt;/p&gt;

&lt;p&gt;A fallback does not always mean silently selecting another provider. Different routes can have different asset representations, costs, and conditions. In some situations, stopping the action and asking the user to review a new quote is safer than automatically changing the path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three practical expansion models
&lt;/h2&gt;

&lt;p&gt;Teams can approach broader network access in several ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate network deployments
&lt;/h3&gt;

&lt;p&gt;The protocol launches on multiple chains, with local liquidity on each. This offers gradual expansion and a familiar experience, but markets remain fragmented.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aggregated liquidity access
&lt;/h3&gt;

&lt;p&gt;The interface compares or combines routes from existing venues. This can improve asset coverage without requiring the protocol to create every pool, but it increases dependency on external data and execution sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-chain transaction flow
&lt;/h3&gt;

&lt;p&gt;The product coordinates movement and exchange across networks in one journey. This offers the most unified experience, but also creates the greatest demands on routing, status tracking, recovery, security, and support.&lt;/p&gt;

&lt;p&gt;A roadmap can use more than one model over time. The important point is to describe accurately what the product does at each stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use evidence to choose the second network
&lt;/h2&gt;

&lt;p&gt;The first expansion should be driven by observable demand.&lt;/p&gt;

&lt;p&gt;Useful signals include users attempting to connect from another network, repeated requests for specific assets, existing community activity, reliable liquidity partners, and a clear volume opportunity. The team should also assess whether users currently leave the product to complete related actions elsewhere.&lt;/p&gt;

&lt;p&gt;An experienced partner in &lt;a href="https://pixelplex.io/services/decentralized-exchange-development-company/" rel="noopener noreferrer"&gt;decentralized crypto exchange development&lt;/a&gt; can help compare expansion models, map dependencies, and estimate how each option changes the product and operating scope. The goal should be a controlled roadmap, not the largest possible network list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expand only when the experience can remain coherent
&lt;/h2&gt;

&lt;p&gt;Single-chain and cross-chain DEXs solve different problems. One offers focus and the opportunity to build deep market quality within an ecosystem. The other can connect fragmented users and assets, but requires stronger routing, communication, monitoring, and recovery design.&lt;/p&gt;

&lt;p&gt;A team should begin with the smallest scope that solves the user’s real obstacle. It can then expand when demand, liquidity, and operational readiness support the next step.&lt;/p&gt;

&lt;p&gt;The most useful DEX is not necessarily the one connected to the most networks. It is the one that lets users understand what they are doing, complete it reliably, and return with confidence.&lt;/p&gt;

</description>
      <category>dex</category>
      <category>defi</category>
      <category>web3</category>
      <category>crypto</category>
    </item>
    <item>
      <title>How to Build a Data Science MVP Without Creating a Maintenance Nightmare</title>
      <dc:creator>Kate Steele</dc:creator>
      <pubDate>Wed, 02 Sep 2026 14:31:11 +0000</pubDate>
      <link>https://dev.to/kate_steeleeee/how-to-build-a-data-science-mvp-without-creating-a-maintenance-nightmare-12jb</link>
      <guid>https://dev.to/kate_steeleeee/how-to-build-a-data-science-mvp-without-creating-a-maintenance-nightmare-12jb</guid>
      <description>&lt;p&gt;A data science MVP is often interpreted as “train a model quickly and show a demo.” That approach can validate that an algorithm finds patterns, but it does not validate whether the product can deliver a useful decision repeatedly.&lt;br&gt;
A better MVP is a thin vertical slice. It uses real input data, runs through a repeatable pipeline, produces an output for a real user or system, and captures enough feedback to judge whether the idea deserves further investment. It stays small without becoming disposable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the smallest useful decision
&lt;/h2&gt;

&lt;p&gt;Start with one user, one decision, and one outcome.&lt;br&gt;
“Predict customer behavior” is not an MVP scope. “Rank accounts for the weekly retention review and measure how many flagged accounts renew after intervention” is bounded enough to test.&lt;br&gt;
Write down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who receives the result;&lt;/li&gt;
&lt;li&gt;when they receive it;&lt;/li&gt;
&lt;li&gt;what action they can take;&lt;/li&gt;
&lt;li&gt;how many cases they can handle;&lt;/li&gt;
&lt;li&gt;what happens when the system is uncertain;&lt;/li&gt;
&lt;li&gt;which outcome will be measured.
This definition prevents the team from building features that look impressive but do not affect the decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prove value with a baseline first
&lt;/h2&gt;

&lt;p&gt;Before training a complex model, create the simplest credible baseline. It may be a business rule, a rolling average, a linear model, or the current manual process.&lt;br&gt;
A baseline serves three purposes. It confirms that the evaluation pipeline works. It reveals whether sophisticated modeling is necessary. It gives stakeholders a clear comparison.&lt;br&gt;
For example, a demand forecasting MVP might compare:&lt;br&gt;
&lt;code&gt;Baseline A: same value as last week&lt;br&gt;
Baseline B: average of the previous four weeks&lt;br&gt;
Candidate: model using seasonality, promotions, and stock signals&lt;/code&gt;&lt;br&gt;
If the candidate produces only a small technical improvement and requires much more maintenance, the baseline may be the better product. Complexity should earn its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build one vertical slice
&lt;/h2&gt;

&lt;p&gt;The MVP architecture can be compact:&lt;br&gt;
&lt;code&gt;source extract&lt;br&gt;
    -&amp;gt; validation&lt;br&gt;
    -&amp;gt; feature generation&lt;br&gt;
    -&amp;gt; baseline and candidate model&lt;br&gt;
    -&amp;gt; stored predictions&lt;br&gt;
    -&amp;gt; lightweight user view or API&lt;br&gt;
    -&amp;gt; outcome capture&lt;/code&gt;&lt;br&gt;
Each arrow should be executable without manual notebook steps. The pipeline may run from a scheduled command, but it should record the input version, configuration, and output location.&lt;br&gt;
Avoid building a general-purpose platform before the first use case is proven. At the same time, do not hide critical logic inside an analyst’s local environment. The goal is a narrow system with clean boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the repository boring
&lt;/h2&gt;

&lt;p&gt;A predictable structure makes the MVP easier to review and extend:&lt;br&gt;
&lt;code&gt;project/&lt;br&gt;
├── config/&lt;br&gt;
│   ├── development.yaml&lt;br&gt;
│   └── production.yaml&lt;br&gt;
├── data_contracts/&lt;br&gt;
├── src/&lt;br&gt;
│   ├── ingest.py&lt;br&gt;
│   ├── validate.py&lt;br&gt;
│   ├── features.py&lt;br&gt;
│   ├── train.py&lt;br&gt;
│   ├── predict.py&lt;br&gt;
│   └── evaluate.py&lt;br&gt;
├── tests/&lt;br&gt;
├── notebooks/&lt;br&gt;
├── artifacts/&lt;br&gt;
└── README.md&lt;/code&gt;&lt;br&gt;
Notebooks belong in the project, but they should not be the only place where transformations or model logic exist. Move reusable code into modules as soon as it affects a result that others need to reproduce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define interfaces before implementations
&lt;/h2&gt;

&lt;p&gt;A stable interface lets the team replace the model without changing every consumer.&lt;br&gt;
&lt;code&gt;from dataclasses import dataclass&lt;br&gt;
from typing import Protocol&lt;br&gt;
@dataclass(frozen=True)&lt;br&gt;
class Prediction:&lt;br&gt;
    entity_id: str&lt;br&gt;
    score: float&lt;br&gt;
    model_version: str&lt;br&gt;
    status: str&lt;br&gt;
class Scorer(Protocol):&lt;br&gt;
    def score(&lt;br&gt;
        self,&lt;br&gt;
        entity_id: str,&lt;br&gt;
        features: dict[str, float],&lt;br&gt;
    ) -&amp;gt; Prediction:&lt;br&gt;
        ...&lt;/code&gt;&lt;br&gt;
The first implementation could wrap a rules engine. The second could use a statistical model. The dashboard or API only depends on the Prediction contract.&lt;br&gt;
This separation also makes fallback behavior easier. When the candidate model is unavailable, the application can call the baseline scorer and mark the result accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate data before optimizing models
&lt;/h2&gt;

&lt;p&gt;MVP teams sometimes spend days tuning parameters while the input pipeline quietly produces duplicates, stale records, or inconsistent labels.&lt;br&gt;
Add a small number of high-value checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;required columns exist;&lt;/li&gt;
&lt;li&gt;identifiers are not unexpectedly null;&lt;/li&gt;
&lt;li&gt;timestamps fall within an expected range;&lt;/li&gt;
&lt;li&gt;key values are unique where required;&lt;/li&gt;
&lt;li&gt;target labels follow a documented definition;&lt;/li&gt;
&lt;li&gt;row counts and missing-value rates remain plausible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Failing the pipeline is often better than publishing predictions based on corrupted input. For lower-risk use cases, the system may continue with a warning, but that choice should be explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use time-aware evaluation
&lt;/h2&gt;

&lt;p&gt;Random train-test splits can create misleading results when behavior changes over time. For many business problems, training on earlier periods and evaluating on later periods better reflects deployment.&lt;br&gt;
The split should also match how predictions will be generated. If the product scores customers every Monday, create historical snapshots that use only information available by each Monday. This avoids leakage from future events.&lt;br&gt;
Do not report only one aggregate metric. Review performance across periods and relevant segments. A model that looks good overall may fail for a region or product line that matters operationally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship the output into an existing workflow
&lt;/h2&gt;

&lt;p&gt;An MVP does not need a full product interface. A scheduled table, a simple internal page, or an API connected to an existing tool may be enough.&lt;br&gt;
The important requirement is that a real user can act on the result. Sending a CSV by email may be acceptable for an early test, but the process should still capture which version generated it and what users did with the recommendations.&lt;br&gt;
The delivery format should expose uncertainty. A ranked queue, confidence band, or “manual review required” status is often more useful than an unexplained yes-or-no label.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add feedback as a first-class feature
&lt;/h2&gt;

&lt;p&gt;Without outcome and user feedback, the team can measure model behavior but not product value.&lt;br&gt;
Capture whether the recommendation was accepted, overridden, or impossible to use. Record the eventual outcome when it becomes available. Keep feedback fields structured enough to analyze, but allow a short note when context matters.&lt;br&gt;
This reveals whether the problem is the model, the workflow, or the available action. A highly accurate recommendation can still fail if users receive it too late or lack authority to respond.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate only what the risk permits
&lt;/h2&gt;

&lt;p&gt;For an MVP, human review is often an advantage. It limits harm, generates labeled feedback, and exposes edge cases. Automation can expand after the team understands failure modes and establishes monitoring.&lt;br&gt;
Define an abstention path. The model should be able to say that it lacks sufficient information. Route those cases to a baseline or a person rather than forcing a low-confidence answer.&lt;br&gt;
When a project requires coordinated discovery, engineering, modeling, integration, and release planning, external &lt;a href="https://pixelplex.io/services/data-science-company/" rel="noopener noreferrer"&gt;data science development services&lt;/a&gt; can accelerate the vertical slice. The deliverable should still remain transparent, documented, and transferable to the internal team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failure paths
&lt;/h2&gt;

&lt;p&gt;Happy-path tests are not enough. An MVP should answer practical questions:&lt;br&gt;
What happens when the source file is empty? What if a category appears that the model has never seen? Can the pipeline be rerun without duplicate predictions? Can the previous model version be restored? Does the consumer know when a score is stale?&lt;br&gt;
A focused test suite can cover:&lt;br&gt;
&lt;code&gt;schema tests&lt;br&gt;
feature transformation tests&lt;br&gt;
model interface tests&lt;br&gt;
pipeline integration tests&lt;br&gt;
fallback tests&lt;/code&gt;&lt;br&gt;
You do not need exhaustive coverage, but you do need confidence in the parts that could silently change a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set exit criteria before the demo
&lt;/h2&gt;

&lt;p&gt;An MVP should end with a decision: scale, revise, or stop. Define the criteria before stakeholders see a polished interface.&lt;br&gt;
Useful criteria may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;improvement over the baseline;&lt;/li&gt;
&lt;li&gt;sufficient data quality and coverage;&lt;/li&gt;
&lt;li&gt;acceptable operational workload;&lt;/li&gt;
&lt;li&gt;evidence that users act on the output;&lt;/li&gt;
&lt;li&gt;measurable movement in the target outcome;&lt;/li&gt;
&lt;li&gt;a feasible path to security, monitoring, and ownership.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A technically successful experiment may still be stopped if the intervention is too costly or the data can’t be maintained. That is a valuable result because it prevents a larger investment in the wrong system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to build yet
&lt;/h2&gt;

&lt;p&gt;Delay components that do not reduce the main uncertainty. The MVP probably does not need a multi-model feature platform, elaborate real-time serving, automated retraining, or a custom analytics portal.&lt;br&gt;
Add infrastructure when the use case proves that scale, latency, reuse, or governance requires it. Until then, favor simple scheduled jobs, versioned files or tables, clear contracts, and visible logs.&lt;br&gt;
Small does not mean careless. It means every component exists for a reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A maintainable data science MVP is not a miniature enterprise platform and not a disposable notebook. It is a narrow, repeatable decision loop built with enough engineering discipline to produce trustworthy evidence.&lt;br&gt;
Start with a baseline, ship one vertical slice, capture real feedback, and test failure behavior. Then scale only the parts that the evidence says are worth keeping.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>mvp</category>
      <category>machinelearning</category>
      <category>development</category>
    </item>
    <item>
      <title>A Practical Guide to Database Migration Without Operational Chaos</title>
      <dc:creator>Kate Steele</dc:creator>
      <pubDate>Tue, 01 Sep 2026 14:40:29 +0000</pubDate>
      <link>https://dev.to/kate_steeleeee/a-practical-guide-to-database-migration-without-operational-chaos-563m</link>
      <guid>https://dev.to/kate_steeleeee/a-practical-guide-to-database-migration-without-operational-chaos-563m</guid>
      <description>&lt;p&gt;Database migration is often summarized as moving data from one system to another. That description is technically correct and operationally incomplete.&lt;/p&gt;

&lt;p&gt;A database carries business meaning: relationships, permissions, historical states, and assumptions accumulated over years. The main risk is not failed copying, but a complete-looking system that interprets the business differently.&lt;/p&gt;

&lt;p&gt;A reliable migration is a controlled product transition, not a single technical event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Begin with the business reason
&lt;/h2&gt;

&lt;p&gt;Migration is disruptive. The team should be able to explain why the current system no longer supports the business and what the new one must improve.&lt;/p&gt;

&lt;p&gt;Common reasons include performance limits, difficult scaling, unsupported technology, rising maintenance cost, weak access control, poor integration options, inconsistent data, or the need to support a redesigned product.&lt;/p&gt;

&lt;p&gt;The reason determines the plan. A migration driven by security risk may prioritize rapid retirement of the old platform. A migration driven by a new data model may require more time for interpretation and business validation. A performance project may keep much of the existing structure while changing the operating environment.&lt;/p&gt;

&lt;p&gt;Success criteria should be measurable. “Move to the cloud” describes an activity. “Reduce failed peak-hour transactions while preserving reconciliation accuracy and shortening recovery time” describes an outcome.&lt;/p&gt;

&lt;p&gt;Without a clear reason, the migration can become a long program that reproduces the old system without improving it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inventory every dependency, not only every table
&lt;/h2&gt;

&lt;p&gt;Applications rarely access a database through one clean path. Customer-facing services, internal tools, scheduled reports, scripts, integrations, exports, monitoring jobs, and individual analysts may all depend on it.&lt;/p&gt;

&lt;p&gt;Some dependencies are documented; others appear only when they fail.&lt;/p&gt;

&lt;p&gt;The inventory should identify systems that read or write data, their owners, reports, scheduled processes, permission dependencies, external partners, availability requirements, and business periods when disruption is especially costly.&lt;/p&gt;

&lt;p&gt;Connection logs, code search, interviews, and operational observation can all reveal dependencies.&lt;/p&gt;

&lt;p&gt;The goal is not merely to produce a list. Each dependency needs a migration decision: update, replace, retire, or temporarily support through a compatibility layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the source of truth during every phase
&lt;/h2&gt;

&lt;p&gt;A migration creates a period in which old and new systems may both contain current information. Unless ownership is explicit, teams can update different versions and create conflicts that are difficult to reconcile.&lt;/p&gt;

&lt;p&gt;For each stage, define which system is authoritative for each record or workflow. The old platform may remain the source of truth while data is copied to the new one for testing. Later, one customer group or process may switch while the rest stays on the old system.&lt;/p&gt;

&lt;p&gt;Dual writing should be treated carefully. Sending every update to two databases sounds reassuring, but partial failures can produce divergence. If dual writing is necessary, the team needs an operation log, retry strategy, reconciliation process, and a clear rule for resolving conflicts.&lt;/p&gt;

&lt;p&gt;Temporary architecture still needs production-level discipline because the business will depend on it during the most sensitive part of the transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean data before structure hides the problem
&lt;/h2&gt;

&lt;p&gt;Legacy databases contain valid history alongside duplicates, missing identifiers, outdated categories, and records that no longer match current rules. Moving all of this unchanged may make the technical cutover faster while preserving the reasons the old system was difficult to use.&lt;/p&gt;

&lt;p&gt;Migration creates a valuable opportunity to classify data.&lt;/p&gt;

&lt;p&gt;Some records should be transformed into the new model. Some should be corrected or merged. Some should remain in an archive. Some may need to be deleted under retention rules. Ambiguous cases should be reviewed by business owners rather than decided through a generic script.&lt;/p&gt;

&lt;p&gt;Cleaning does not mean rewriting history to make it look tidy. Corrections should remain traceable, especially for financial, regulated, or customer-impacting information.&lt;/p&gt;

&lt;p&gt;Transformation rules and exceptions should be recorded so migrated values can be explained later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Map meaning, not only fields
&lt;/h2&gt;

&lt;p&gt;Field-to-field mapping is necessary but insufficient.&lt;/p&gt;

&lt;p&gt;The same label may have different meanings in two systems. A legacy “active” status might include customers who are trialing, paying, paused, or awaiting review. The new model may separate those states. One old customer record may become an organization, several contacts, multiple subscriptions, and a billing profile.&lt;/p&gt;

&lt;p&gt;This is not a copying decision. It is a domain decision.&lt;/p&gt;

&lt;p&gt;Mappings should describe business interpretation, defaults, required enrichment, exceptions, and how relationships and historical states will be preserved.&lt;/p&gt;

&lt;p&gt;When teams focus only on column names and data types, they risk delivering technically valid records that produce incorrect workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build migration runs to be repeatable
&lt;/h2&gt;

&lt;p&gt;The first migration run should not be the production cutover. Teams need repeated rehearsals using representative data and the same procedures intended for launch.&lt;/p&gt;

&lt;p&gt;A repeatable process extracts a defined source version, applies documented transformations, loads the target environment, produces validation results, and records rejected or changed data.&lt;/p&gt;

&lt;p&gt;Rerunning it should not create duplicates or depend on hidden manual steps. Necessary manual decisions should be captured so they can be reapplied.&lt;/p&gt;

&lt;p&gt;Repeated runs reveal how long the move takes, where failures cluster, and whether the process behaves consistently as source data changes. They also make rollback and recovery more credible because the team has practiced the operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate business behavior, not only data volume
&lt;/h2&gt;

&lt;p&gt;Matching row counts can prove that a certain number of records arrived. It cannot prove that the new system behaves correctly.&lt;/p&gt;

&lt;p&gt;Validation should cover several layers.&lt;/p&gt;

&lt;p&gt;Structural checks confirm required fields, types, identifiers, and relationships. Reconciliation checks compare balances, totals, status distributions, and record counts. Application checks confirm that important workflows can read and update the migrated data. Permission checks verify that users see only what their roles allow. Historical checks confirm that past states and timestamps still mean what the business expects.&lt;/p&gt;

&lt;p&gt;Business users should review representative and difficult cases. They know which exceptions are meaningful and which reports must agree before the system can be trusted.&lt;/p&gt;

&lt;p&gt;Validation thresholds should be defined before the final run. Otherwise, teams may lower expectations under launch pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan the cutover as a sequence
&lt;/h2&gt;

&lt;p&gt;The cutover plan should specify what happens before, during, and after traffic moves.&lt;/p&gt;

&lt;p&gt;Before the cutover, the team may freeze selected changes, complete a final incremental copy, verify system health, and confirm that support teams are ready. During the transition, traffic may move by customer segment, geography, workflow, or percentage. Afterward, the team monitors application behavior, data reconciliation, and user-reported issues.&lt;/p&gt;

&lt;p&gt;Some systems require a single switch. Even then, the operation should be divided into checkpoints with explicit go or no-go decisions.&lt;/p&gt;

&lt;p&gt;Employees and partners need to know which actions are unavailable, when data may appear delayed, and where to report unexpected behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollback must restore a coherent business state
&lt;/h2&gt;

&lt;p&gt;“Switch back if something fails” is not a complete rollback plan.&lt;/p&gt;

&lt;p&gt;If the new system has accepted writes, the old system may no longer contain the latest state. Returning traffic without reconciling those changes can lose orders, approvals, payments, or user updates.&lt;/p&gt;

&lt;p&gt;The rollback design should answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What conditions trigger rollback?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Who has authority to make the decision?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How are writes captured during the transition?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can new records be replayed into the old system?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Which integrations must switch back?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How will users be informed?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How will the team confirm that the restored state is coherent?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes the safer response is to pause a workflow while the team repairs the new system rather than return to an outdated source. The correct choice depends on the business process and must be decided before the incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security should improve during the move
&lt;/h2&gt;

&lt;p&gt;Migration can temporarily increase exposure because teams create exports, staging environments, logs, backups, and access paths that do not exist during normal operations.&lt;/p&gt;

&lt;p&gt;Sensitive data should remain protected throughout extraction, transfer, testing, and disposal. Access should be limited to the people and services that need it. Test environments should not receive production data by default without appropriate controls. Temporary copies should have owners and deletion dates.&lt;/p&gt;

&lt;p&gt;The target system’s roles and permissions need direct validation. A technically successful migration that grants broader access is not successful.&lt;/p&gt;

&lt;p&gt;Auditability is also important. Teams should know who initiated runs, which data version was used, what transformations occurred, and which records required manual intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose outside support for migration discipline
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://pixelplex.io/services/database-development-company/" rel="noopener noreferrer"&gt;database development company&lt;/a&gt; can be valuable when the transition combines schema redesign, data cleansing, system integration, performance requirements, and high availability. The selection should focus on the migration method, not only familiarity with the target technology.&lt;/p&gt;

&lt;p&gt;A capable partner should ask about business-critical periods, hidden consumers, historical requirements, access rules, rollback, reconciliation, and post-launch ownership. It should be able to explain how it will rehearse the migration and prove that the result is correct.&lt;/p&gt;

&lt;p&gt;The organization should retain ownership of business definitions and acceptance criteria. External engineers cannot resolve an ambiguous status or duplicate customer without domain input.&lt;/p&gt;

&lt;p&gt;Documentation and knowledge transfer should be included from the beginning so that the new platform does not become another system understood only by a small external group.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not retire the old system immediately
&lt;/h2&gt;

&lt;p&gt;After cutover, the legacy database may need to remain available in a controlled, read-only state while teams verify historical queries, reports, and edge cases.&lt;/p&gt;

&lt;p&gt;This period should have a defined end. Keeping the old system indefinitely creates cost and uncertainty. Retirement should cover final backups, access removal, integration shutdown, and disposal of temporary migration data.&lt;/p&gt;

&lt;p&gt;Post-migration work should also include performance tuning based on real usage, review of failed or manually transformed records, and confirmation that monitoring and support procedures operate as expected.&lt;/p&gt;

&lt;p&gt;The migration is complete when the organization can run the new system confidently, not simply when the first production request succeeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Database migration is difficult because data is inseparable from the business processes that created it. A reliable transition must preserve relationships, history, permissions, and operational continuity while introducing a new foundation.&lt;/p&gt;

&lt;p&gt;The safest programs define the business reason, inventory dependencies, rehearse migration runs, validate real workflows, and prepare a rollback that protects the current state.&lt;/p&gt;

&lt;p&gt;Treating migration as a controlled sequence of decisions turns it from a high-risk weekend event into an evidence-based product change.&lt;/p&gt;

</description>
      <category>database</category>
      <category>dataengineering</category>
      <category>devops</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Web3 Application Development: Designing Products Around Ownership, Composability, and Verifiable State</title>
      <dc:creator>Kate Steele</dc:creator>
      <pubDate>Mon, 31 Aug 2026 14:10:46 +0000</pubDate>
      <link>https://dev.to/kate_steeleeee/web3-application-development-designing-products-around-ownership-composability-and-verifiable-5625</link>
      <guid>https://dev.to/kate_steeleeee/web3-application-development-designing-products-around-ownership-composability-and-verifiable-5625</guid>
      <description>&lt;p&gt;Traditional applications usually rely on one company-controlled database. Web3 changes that model by letting important state live on a blockchain, where ownership and transaction rules can be verified independently.&lt;/p&gt;

&lt;p&gt;That makes &lt;a href="https://pixelplex.io/services/web3-app-development-company/" rel="noopener noreferrer"&gt;Web3 application development&lt;/a&gt; more than ordinary software with smart contracts added underneath. The real challenge is deciding what belongs on-chain, what should remain in conventional infrastructure, and how those layers can work together without making the product difficult to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes when application state moves on-chain
&lt;/h2&gt;

&lt;p&gt;The biggest difference between Web2 and Web3 is not the wallet. It is where authority over important state lives. Once ownership or transaction logic becomes on-chain, users and other applications can rely on the same public record.&lt;/p&gt;

&lt;p&gt;That creates stronger transparency, but it also makes product rules harder to change casually. The development team has to think about long-term behavior much earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ownership becomes part of the architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a conventional application, an asset usually belongs to someone because the company database says so. In Web3, ownership can be represented through a blockchain token that other compatible applications can also recognize.&lt;/p&gt;

&lt;p&gt;This changes the product model. The team needs to define what ownership actually allows, whether assets can move freely, and what happens when their state changes. Putting ownership on-chain gives users stronger control, but it also makes those rules more visible and harder to rewrite later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Public state changes how rules are designed
&lt;/h2&gt;

&lt;p&gt;When a smart contract controls rewards or transfers, users can verify the same logic the application relies on. That makes some product behavior more transparent than it would be inside a private backend.&lt;/p&gt;

&lt;p&gt;Not every rule needs this treatment. Frequently changing features can remain off-chain when blockchain adds little value. The strongest architecture puts only the rules that benefit from independent verification on-chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composability turns protocols into building blocks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Web3 applications can interact with existing smart contracts instead of rebuilding every capability. A new product may connect to an established exchange protocol or use another protocol for lending or asset management.&lt;/p&gt;

&lt;p&gt;This can speed up development, but it also creates dependency risk. If an external protocol changes or becomes unavailable, the application may be affected even when its own code remains stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture behind a Web3 application
&lt;/h2&gt;

&lt;p&gt;Most Web3 applications are hybrid systems. Smart contracts provide the trusted execution layer, while other infrastructure handles faster data access and ordinary product logic.&lt;/p&gt;

&lt;p&gt;The important part is keeping those responsibilities clear. Supporting services can improve usability without replacing the blockchain as the authoritative source for on-chain state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smart contracts act as the rule engine&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Smart contracts should control the behavior that users need to verify independently. This may include ownership or settlement logic, while high-frequency application behavior can remain elsewhere.&lt;/p&gt;

&lt;p&gt;Keeping the contract layer focused makes it easier to test and maintain. It also reduces the number of actions that require blockchain transactions, which can improve both cost and user experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Indexing turns blockchain history into usable data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Blockchains preserve transaction history well, but applications often need faster ways to query that information. Indexing systems process events into structured data so the frontend can display balances or activity without rebuilding everything directly from the chain.&lt;/p&gt;

&lt;p&gt;The index should improve access, not become a competing source of truth. If it falls behind, the application should recognize that delay instead of showing stale information as confirmed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decentralized storage extends the ownership model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Large media files or detailed metadata are usually impractical to store directly on-chain. Content-addressed storage can provide a better fit by linking blockchain assets to information stored outside the chain.&lt;/p&gt;

&lt;p&gt;This still requires planning. A content identifier proves which file is referenced, but the file also needs to remain available. Persistence therefore remains an operational responsibility even when storage is decentralized.&lt;/p&gt;

&lt;h2&gt;
  
  
  Product decisions unique to Web3 applications
&lt;/h2&gt;

&lt;p&gt;Architecture choices eventually become user experience. Account design changes onboarding, while transaction design determines how much blockchain friction users actually feel.&lt;/p&gt;

&lt;p&gt;A strong Web3 product hides unnecessary complexity without hiding the consequences of important actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Account design shapes onboarding&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Crypto-native applications can rely on users bringing their own wallets. Mainstream products may need smart-account models that reduce dependence on seed phrases and native gas tokens.&lt;/p&gt;

&lt;p&gt;Account abstraction can support capabilities such as gas sponsorship and transaction batching. The best model depends on the audience. A trading tool for experienced users and a consumer app should not automatically use the same onboarding flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transactions need a visible lifecycle&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Signing a transaction is not the same as confirmation. A transaction can remain pending before it succeeds or fails, so the interface needs to communicate those states clearly.&lt;/p&gt;

&lt;p&gt;This is especially important when value is involved. Users should know whether an action is still waiting for the network or whether it has reached its final state, otherwise they may repeat the same transaction unnecessarily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permissions need to be understandable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Web3 users may grant token allowances or approve smart-account permissions. Contracts can also contain administrative roles that affect the system after launch.&lt;/p&gt;

&lt;p&gt;The application should explain these permissions in product language. A technically correct signing request is not enough if the user cannot understand what authority they are giving away or whether it can later be revoked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decentralization should be selective&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Web3 application does not need every service to be decentralized. The useful question is which form of control users benefit from moving away from one central operator.&lt;/p&gt;

&lt;p&gt;For some products, that is custody. For others, it is transparent settlement. Selective decentralization usually produces a cleaner product than moving routine functionality on-chain simply to make the architecture appear more Web3-native.&lt;/p&gt;

&lt;h2&gt;
  
  
  Composability and long-term evolution
&lt;/h2&gt;

&lt;p&gt;Open protocols give Web3 applications access to infrastructure they did not build themselves. That can shorten development dramatically, but it also means the product inherits some of the assumptions of every external protocol it uses.&lt;/p&gt;

&lt;p&gt;The application also needs a plan for change. Smart contracts may be difficult to replace once other users or applications depend on them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;External protocols need their own risk model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Integrating an existing protocol can be more efficient than rebuilding the same functionality internally. The team still needs to understand what the application depends on and what happens if that dependency changes.&lt;/p&gt;

&lt;p&gt;This is particularly important with upgradeable contracts or liquidity-dependent services. The integration may remain technically available while the economic conditions around it become unsuitable for users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-chain features multiply application states&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A single-chain application has one primary execution environment. Cross-chain functionality introduces periods where one network has completed an action while another is still waiting.&lt;/p&gt;

&lt;p&gt;The application needs to represent that intermediate state clearly. Developers also need protection against duplicate execution and out-of-order messages, making multichain development a state-management problem rather than simply a matter of adding more RPC endpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upgrades need an explicit trust model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Upgradeable contracts make future fixes possible, but someone must retain authority to change the implementation. That power should be visible and governed deliberately.&lt;/p&gt;

&lt;p&gt;Immutable contracts avoid that authority but may eventually need migration to a replacement. Either way, the product needs a clear plan for how users and connected applications will move when the system changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production monitoring has to cover the full stack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Web3 application can appear broken even when its smart contracts are healthy. RPC services can fail, indexers can lag, and external dependencies can stop responding.&lt;/p&gt;

&lt;p&gt;Monitoring should therefore connect blockchain activity with application behavior. The team needs to distinguish a contract problem from an infrastructure problem quickly, especially when users depend on the system for valuable assets or transactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Web3 application development is fundamentally about deciding where trust should live. Smart contracts can make ownership and execution independently verifiable, while composability lets products build on open infrastructure that already exists.&lt;/p&gt;

&lt;p&gt;The strongest applications use those properties selectively. Blockchain remains authoritative where verification matters, while conventional software keeps the product fast and flexible. When account design, dependencies, and long-term evolution are planned together, Web3 becomes a coherent application architecture rather than a collection of blockchain features.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
