<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DupeHell — Synthetic Data Generator for Record Linkage Benchmarking</title>
<link rel="icon" type="image/png" href="docs/assets/logo_dupehell-3.png">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#0a0e17;
--surface:#131a2b;
--elevated:#1a2338;
--border:#1e2a45;
--cyan:#00d4aa;
--amber:#f0b429;
--blue:#4a8cff;
--text:#e2e8f0;
--muted:#8892a4;
--code:#a8b8d8;
--danger:#ef4444;
--success:#22d68c;
--radius:8px;
--ff:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;
--fm:"SF Mono","Fira Code","Cascadia Code","JetBrains Mono",Consolas,monospace;
}
html{scroll-behavior:smooth}
body{font-family:var(--ff);background:var(--bg);color:var(--text);line-height:1.6;-webkit-font-smoothing:antialiased;overflow-x:hidden}
a{color:var(--blue);text-decoration:none}
a:hover{text-decoration:underline}
::selection{background:rgba(0,212,170,.3);color:#fff}
body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse 80% 60% at 50% -10%,rgba(0,212,170,.06) 0,transparent 70%),radial-gradient(ellipse 60% 50% at 80% 80%,rgba(74,140,255,.04) 0,transparent 60%),radial-gradient(ellipse 50% 40% at 20% 90%,rgba(240,180,41,.03) 0,transparent 50%);pointer-events:none;z-index:0}
.container{max-width:1200px;margin:0 auto;padding:0 24px;position:relative;z-index:1}
.reveal{opacity:0;transform:translateY(30px);transition:opacity .7s ease,transform .7s ease}
.reveal.visible{opacity:1;transform:translateY(0)}
.hero{min-height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:40px 0;position:relative;text-align:center}
.hero-wordmark{position:relative;margin-bottom:8px}
.hero-logo{width:100%;max-width:600px;height:auto;filter:invert(1) drop-shadow(0 0 12px rgba(0,212,170,.25));display:block}
.hero-tagline{font-size:clamp(1rem,2vw,1.3rem);color:var(--muted);margin-bottom:32px;font-weight:400}
.hero-badges{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:36px;justify-content:center}
.hero-badge{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 24px;display:flex;flex-direction:column;align-items:center;min-width:140px;transition:border-color .3s,box-shadow .3s}
.hero-badge:hover{border-color:rgba(0,212,170,.3);box-shadow:0 0 25px rgba(0,212,170,.08)}
.hero-badge .num{font-family:var(--fm);font-size:1.8rem;font-weight:700;color:var(--cyan);line-height:1.2}
.hero-badge .lbl{font-size:.8rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
.install-bar{display:flex;align-items:center;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:4px;max-width:520px;width:100%;margin:0 auto 24px}
.install-bar code{flex:1;padding:12px 16px;font-family:var(--fm);font-size:.9rem;color:var(--code);background:transparent;border:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.install-bar .copy-btn{background:var(--cyan);border:none;color:#0a0e17;font-family:var(--fm);font-size:.75rem;font-weight:600;padding:8px 16px;border-radius:6px;cursor:pointer;transition:background .2s,transform .15s;text-transform:uppercase;letter-spacing:.04em}
.install-bar .copy-btn:hover{background:#00e6b8;transform:scale(1.02)}
.install-bar .copy-btn:active{transform:scale(.97)}
.install-bar .copy-btn.copied{background:var(--success);color:#0a0e17}
.hero-ethics{position:absolute;bottom:24px;right:24px;font-size:.7rem;color:var(--muted);background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.2);padding:6px 14px;border-radius:20px;display:inline-flex;align-items:center;gap:6px}
.hero-ethics svg{width:12px;height:12px;fill:none;stroke:var(--danger);stroke-width:2}
.quickstart{padding:80px 0}
.quickstart h2{font-family:var(--fm);font-size:1.5rem;margin-bottom:8px;color:var(--text)}
.quickstart .sub{color:var(--muted);margin-bottom:32px;font-size:.95rem}
.qs-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.code-panel{background:#050810;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
.code-panel .panel-head{display:flex;align-items:center;gap:8px;padding:10px 16px;background:rgba(255,255,255,.03);border-bottom:1px solid var(--border)}
.code-panel .panel-head .dot{width:10px;height:10px;border-radius:50%;opacity:.6}
.code-panel .panel-head .dot.r{background:#ef4444}
.code-panel .panel-head .dot.y{background:var(--amber)}
.code-panel .panel-head .dot.g{background:var(--success)}
.code-panel .panel-head .label{font-family:var(--fm);font-size:.75rem;color:var(--muted);margin-left:auto;text-transform:uppercase;letter-spacing:.05em}
.code-panel pre{padding:20px;overflow-x:auto;font-family:var(--fm);font-size:.82rem;line-height:1.7;color:var(--code);tab-size:2}
.code-panel pre .kw{color:#c792ea}
.code-panel pre .fn{color:#82aaff}
.code-panel pre .str{color:#c3e88d}
.code-panel pre .cm{color:#546e7a;font-style:italic}
.code-panel pre .op{color:#89ddff}
.code-panel pre .prompt{color:var(--success);user-select:none}
.code-panel pre .flag{color:var(--amber)}
.benchmarks{padding:80px 0}
.benchmarks h2{font-family:var(--fm);font-size:1.8rem;margin-bottom:6px}
.benchmarks .sub{color:var(--muted);margin-bottom:20px;font-size:.9rem;max-width:600px}
.bench-top{margin-bottom:32px}
.chart-wrap{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px}
.chart-wrap h3{font-family:var(--fm);font-size:.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:16px}
.chart-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
.chart-header h3{margin-bottom:0}
.chart-legend{display:flex;align-items:center;gap:12px;font-family:var(--fm);font-size:.7rem;color:var(--code)}
.chart-legend span{display:flex;align-items:center;gap:4px}
.chart-legend .dot{width:7px;height:7px;border-radius:2px}
.chart-wrap svg{width:100%;height:auto;display:block}
.table-wrap{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow-x:auto;margin-bottom:20px}
.table-wrap table{width:100%;border-collapse:collapse;font-size:.82rem;white-space:nowrap}
.table-wrap th{background:rgba(255,255,255,.03);padding:12px 14px;text-align:left;font-family:var(--fm);font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--border);font-weight:500}
.table-wrap td{padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.04);font-family:var(--fm);color:var(--code);font-size:.8rem}
.table-wrap tr:hover td{background:rgba(0,212,170,.03)}
.table-wrap td.highlight{color:var(--cyan);font-weight:600}
.callout-box{background:linear-gradient(135deg,rgba(0,212,170,.08),rgba(0,212,170,.02));border:1px solid rgba(0,212,170,.2);border-radius:var(--radius);padding:16px 20px;display:flex;align-items:center;gap:12px}
.callout-box .icon{font-size:1.2rem;flex-shrink:0}
.callout-box .txt{font-size:.85rem;color:var(--code)}
.callout-box .txt strong{color:var(--cyan)}
.domains{padding:80px 0}
.domains h2{font-family:var(--fm);font-size:1.5rem;margin-bottom:6px}
.domains .sub{color:var(--muted);margin-bottom:32px;font-size:.9rem}
.domain-grid{display:flex;flex-wrap:wrap;gap:8px}
.domain-pill{display:inline-block;padding:6px 16px;border-radius:20px;font-family:var(--fm);font-size:.78rem;background:var(--surface);border:1px solid var(--border);color:var(--code);transition:all .25s;cursor:default}
.domain-pill:hover{border-color:rgba(0,212,170,.3);box-shadow:0 0 18px rgba(0,212,170,.08);transform:translateY(-1px)}
.domain-pill.kyc{background:rgba(0,212,170,.12);border-color:var(--cyan);color:var(--cyan);font-weight:600}
.features{padding:80px 0}
.features h2{font-family:var(--fm);font-size:1.5rem;margin-bottom:6px}
.features .sub{color:var(--muted);margin-bottom:40px;font-size:.9rem}
.features-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
.feat-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px;transition:border-color .3s,transform .3s}
.feat-card:hover{border-color:rgba(0,212,170,.2);transform:translateY(-3px)}
.feat-card .icon-wrap{width:40px;height:40px;border-radius:8px;background:rgba(0,212,170,.1);display:flex;align-items:center;justify-content:center;margin-bottom:14px}
.feat-card .icon-wrap svg{width:20px;height:20px;stroke:var(--cyan);fill:none;stroke-width:1.8}
.feat-card h3{font-size:.95rem;font-weight:600;margin-bottom:6px;color:var(--text)}
.feat-card p{font-size:.82rem;color:var(--muted);line-height:1.5}
.splink-demo{padding:80px 0}
.splink-demo h2{font-family:var(--fm);font-size:1.5rem;margin-bottom:6px}
.splink-demo .sub{color:var(--muted);margin-bottom:40px;font-size:.9rem}
.splink-box{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:32px}
.splink-numbers{display:grid;grid-template-columns:1fr auto 1fr;gap:24px;align-items:center;margin-bottom:28px}
.splink-num{text-align:center;padding:16px}
.splink-num .label{font-size:.8rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px}
.splink-num .value{font-family:var(--fm);font-size:2.8rem;font-weight:700;line-height:1}
.splink-num .value.achieved{color:var(--muted)}
.splink-num .value.max{color:var(--cyan)}
.splink-divider{width:1px;height:60px;background:var(--border);flex-shrink:0}
.splink-ratio{margin-bottom:20px}
.splink-ratio .ratio-bar{height:8px;border-radius:4px;background:var(--border);overflow:hidden;position:relative}
.splink-ratio .ratio-bar .fill{height:100%;border-radius:4px;background:linear-gradient(90deg,var(--amber),var(--cyan));width:0%;transition:width 1s ease}
.splink-ratio .ratio-bar .fill.animated{width:49.6%}
.splink-ratio .ratio-text{display:flex;justify-content:space-between;margin-top:6px;font-family:var(--fm);font-size:.78rem;color:var(--muted)}
.splink-link{font-size:.85rem;font-family:var(--fm)}
.splink-link a{color:var(--cyan)}
.ethics-banner{padding:60px 0}
.ethics-banner .inner{background:rgba(239,68,68,.06);border-left:4px solid var(--danger);border-radius:var(--radius);padding:24px 32px;display:flex;align-items:center;gap:14px}
.ethics-banner .inner svg{width:20px;height:20px;fill:none;stroke:var(--danger);stroke-width:2;flex-shrink:0}
.ethics-banner .inner .txt{font-size:.9rem;color:var(--text)}
.ethics-banner .inner .txt strong{color:var(--danger)}
.ethics-banner .inner .txt a{color:var(--muted);text-decoration:underline}
.footer{padding:48px 0;border-top:1px solid var(--border)}
.footer-grid{display:grid;grid-template-columns:2fr 1fr 2fr;gap:32px}
.footer h4{font-family:var(--fm);font-size:.8rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px}
.footer-links{list-style:none}
.footer-links li{margin-bottom:6px}
.footer-links a{font-size:.85rem;color:var(--muted);transition:color .2s}
.footer-links a:hover{color:var(--cyan);text-decoration:none}
.footer .bibtex{background:#050810;border:1px solid var(--border);border-radius:6px;padding:14px 16px;font-family:var(--fm);font-size:.72rem;color:var(--code);line-height:1.6;overflow-x:auto;white-space:pre}
.footer .copyright{font-size:.78rem;color:var(--muted);margin-top:24px}
@media(max-width:1024px){
.features-grid{grid-template-columns:repeat(2,1fr)}
}
@media(max-width:768px){
.qs-grid{grid-template-columns:1fr}
.hero-badges{gap:8px}
.hero-badge{padding:12px 16px;min-width:100px}
.hero-badge .num{font-size:1.3rem}
.splink-numbers{grid-template-columns:1fr;gap:12px}
.splink-divider{width:60px;height:1px}
.features-grid{grid-template-columns:repeat(2,1fr)}
.footer-grid{grid-template-columns:1fr 1fr}
.domain-grid{justify-content:center}
.chart-wrap{padding:16px}
}
@media(max-width:480px){
.container{padding:0 16px}
.hero-badge{min-width:85px;padding:10px 12px}
.hero-badge .num{font-size:1.1rem}
.features-grid{grid-template-columns:1fr}
.footer-grid{grid-template-columns:1fr}
.table-wrap{font-size:.72rem}
.table-wrap td,.table-wrap th{padding:8px 10px}
}
</style>
</head>
<body>
<section class="hero">
<div class="container">
<div class="hero-wordmark">
<img src="docs/assets/logo_dupehell-3.png" alt="DupeHell" class="hero-logo">
</div>
<p class="hero-tagline">Synthetic data generator for record linkage benchmarking</p>
<div class="install-bar">
<code>pip install dupehell</code>
<button class="copy-btn" data-copy="pip install dupehell">Copy</button>
</div>
</div>
</section>
<section class="quickstart" id="quickstart">
<div class="container reveal">
<h2>Quick Start</h2>
<p class="sub">Generate a dataset in two lines of Python or one CLI command.</p>
<div class="qs-grid">
<div class="code-panel">
<div class="panel-head">
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
<span class="label">Python</span>
</div>
<pre><span class="kw">from</span> dupehell <span class="kw">import</span> <span class="fn">generate</span>
<span class="fn">r</span> = <span class="fn">generate</span>(
<span class="flag">domain</span>=<span class="str">"publishing"</span>,
<span class="flag">size</span>=<span class="num">10000</span>,
<span class="flag">seed</span>=<span class="num">42</span>,
<span class="flag">difficulty</span>=<span class="str">"hell"</span>
)
<span class="fn">print</span>(<span class="fn">r</span>.dataset)
<span class="fn">print</span>(<span class="fn">r</span>.ground_truth)
<span class="fn">print</span>(<span class="fn">r</span>.total_records)</pre>
</div>
<div class="code-panel">
<div class="panel-head">
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
<span class="label">CLI</span>
</div>
<pre><span class="prompt">$</span> cargo run --release -- \
<span class="flag">--domain</span> kyc \
<span class="flag">--size</span> <span class="num">100000</span> \
<span class="flag">--seed</span> <span class="num">42</span></pre>
</div>
</div>
</div>
</section>
<section class="benchmarks" id="benchmarks">
<div class="container reveal">
<h2>Performance Benchmarks</h2>
<p class="sub">All runs on Intel Core i7-13850HX (20C/28T), 32 GB DDR5. Identical seed across formats.</p>
<div class="bench-top">
<div class="chart-wrap">
<div class="chart-header">
<h3>Throughput vs Dataset Size (hell difficulty)</h3>
<div class="chart-legend">
<span><span class="dot" style="background:#00d4aa"></span>IPC</span>
</div>
</div>
<svg viewBox="0 0 650 160" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="ipcGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#00d4aa" stop-opacity=".15"/>
<stop offset="100%" stop-color="#00d4aa" stop-opacity="0"/>
</linearGradient>
<linearGradient id="parquetGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f0b429" stop-opacity=".12"/>
<stop offset="100%" stop-color="#f0b429" stop-opacity="0"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<line x1="70" y1="20" x2="620" y2="20" stroke="#1e2a45" stroke-width="1"/>
<line x1="70" y1="50" x2="620" y2="50" stroke="#1e2a45" stroke-width="1"/>
<line x1="70" y1="80" x2="620" y2="80" stroke="#1e2a45" stroke-width="1"/>
<line x1="70" y1="110" x2="620" y2="110" stroke="#1e2a45" stroke-width="1"/>
<line x1="70" y1="140" x2="620" y2="140" stroke="#1e2a45" stroke-width="1"/>
<text x="62" y="24" fill="#8892a4" font-family="SF Mono,Fira Code,monospace" font-size="8" text-anchor="end">800K</text>
<text x="62" y="54" fill="#8892a4" font-family="SF Mono,Fira Code,monospace" font-size="8" text-anchor="end">600K</text>
<text x="62" y="84" fill="#8892a4" font-family="SF Mono,Fira Code,monospace" font-size="8" text-anchor="end">400K</text>
<text x="62" y="114" fill="#8892a4" font-family="SF Mono,Fira Code,monospace" font-size="8" text-anchor="end">200K</text>
<text x="62" y="144" fill="#8892a4" font-family="SF Mono,Fira Code,monospace" font-size="8" text-anchor="end">0</text>
<text x="70" y="158" fill="#8892a4" font-family="SF Mono,Fira Code,monospace" font-size="8" text-anchor="middle">1M</text>
<text x="253" y="158" fill="#8892a4" font-family="SF Mono,Fira Code,monospace" font-size="8" text-anchor="middle">10M</text>
<text x="437" y="158" fill="#8892a4" font-family="SF Mono,Fira Code,monospace" font-size="8" text-anchor="middle">20M</text>
<text x="620" y="158" fill="#8892a4" font-family="SF Mono,Fira Code,monospace" font-size="8" text-anchor="middle">50M</text>
<polygon points="70,39.7 253,36.2 437,38.3 620,45.0 620,140 70,140" fill="url(#ipcGrad)"/>
<polyline points="70,39.7 253,36.2 437,38.3 620,45.0" fill="none" stroke="#00d4aa" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round" filter="url(#glow)"/>
<circle cx="70" cy="39.7" r="4.5" fill="#00d4aa" stroke="#0a0e17" stroke-width="2"/>
<circle cx="253" cy="36.2" r="4.5" fill="#00d4aa" stroke="#0a0e17" stroke-width="2"/>
<circle cx="437" cy="38.3" r="4.5" fill="#00d4aa" stroke="#0a0e17" stroke-width="2"/>
<circle cx="620" cy="45.0" r="4.5" fill="#00d4aa" stroke="#0a0e17" stroke-width="2"/>
<text x="70" y="33" fill="#00d4aa" font-family="SF Mono,Fira Code,monospace" font-size="7" text-anchor="middle">695.5K</text>
<text x="253" y="30" fill="#00d4aa" font-family="SF Mono,Fira Code,monospace" font-size="7" text-anchor="middle">718.8K</text>
<text x="437" y="32" fill="#00d4aa" font-family="SF Mono,Fira Code,monospace" font-size="7" text-anchor="middle">704.9K</text>
<text x="620" y="39" fill="#00d4aa" font-family="SF Mono,Fira Code,monospace" font-size="7" text-anchor="middle">660.0K</text>
</svg>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr><th>Size</th><th>Ø rec/s</th><th>Fastest domain</th><th>Slowest domain</th><th>Range</th></tr>
</thead>
<tbody>
<tr><td>1M</td><td>695,476</td><td>renewable_energy 1.0s</td><td>kyc 3.2s</td><td>2.2s</td></tr>
<tr><td>10M</td><td>718,822</td><td>renewable_energy 9.6s</td><td>kyc 34.7s</td><td>25.1s</td></tr>
<tr><td>20M</td><td>704,888</td><td>renewable_energy 20.1s</td><td>kyc 72.6s</td><td>52.5s</td></tr>
<tr><td>50M</td><td>659,987</td><td>social_media 56.4s</td><td>kyc 165.2s</td><td>108.8s</td></tr>
</tbody>
</table>
</div>
<div class="table-wrap">
<table>
<thead>
<tr><th>Size</th><th>IPC Ø rec/s</th><th>Parquet Ø rec/s</th><th>Overhead</th></tr>
</thead>
<tbody>
<tr><td>1M</td><td>695,476</td><td>650,883</td><td>-6.4%</td></tr>
<tr><td>5M</td><td>734,773</td><td>705,807</td><td>-3.9%</td></tr>
<tr><td>10M</td><td>718,822</td><td>700,238</td><td>-2.6%</td></tr>
<tr><td>20M</td><td>704,888</td><td>690,241</td><td>-2.1%</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section class="domains" id="domains">
<div class="container reveal">
<h2>40 Domains</h2>
<p class="sub">Specialized schemas for entity resolution across industries and use cases.</p>
<div class="domain-grid">
<span class="domain-pill kyc">kyc</span>
<span class="domain-pill">healthcare</span>
<span class="domain-pill">ecommerce</span>
<span class="domain-pill">fintech</span>
<span class="domain-pill">publishing</span>
<span class="domain-pill">gaming</span>
<span class="domain-pill">banking</span>
<span class="domain-pill">telecom</span>
<span class="domain-pill">insurance</span>
<span class="domain-pill">supplychain</span>
<span class="domain-pill" style="color:var(--muted);border-color:var(--muted)">and 30+ more</span>
</div>
</div>
</section>
<section class="features" id="features">
<div class="container reveal">
<h2>Features</h2>
<p class="sub">What makes DupeHell the most comprehensive synthetic ER benchmark generator.</p>
<div class="features-grid">
<div class="feat-card">
<div class="icon-wrap">
<svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
</div>
<h3>Noise Engine</h3>
<p>8 categories, 35+ noise types — typos, OCR errors, homoglyphs, date swaps, phonetic variants, Unicode pollution.</p>
</div>
<div class="feat-card">
<div class="icon-wrap">
<svg viewBox="0 0 24 24"><path d="M12 2L2 12l10 10 10-10L12 2z"/><path d="M8 12l2 2 4-4"/></svg>
</div>
<h3>Hard Negatives</h3>
<p>8 adversarial patterns — same_field, mix_identifier, same_email, mix_arrays, and more — for robust evaluation.</p>
</div>
<div class="feat-card">
<div class="icon-wrap">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
</div>
<h3>40 Domains</h3>
<p>KYC, healthcare, ecommerce, gaming, fintech, publishing, and 34 more — each with a purpose-built schema.</p>
</div>
<div class="feat-card">
<div class="icon-wrap">
<svg viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/></svg>
</div>
<h3>Dual Format</h3>
<p>Arrow IPC for maximum throughput or Parquet ZSTD(3) for compact storage. Same data, identical F1.</p>
</div>
<div class="feat-card">
<div class="icon-wrap">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
</div>
<h3>Deterministic</h3>
<p>Same seed + domain + difficulty = byte-identical output every time. Perfect for reproducible research.</p>
</div>
<div class="feat-card">
<div class="icon-wrap">
<svg viewBox="0 0 24 24"><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><circle cx="12" cy="18" r="3"/><line x1="6" y1="9" x2="12" y2="15"/><line x1="18" y1="9" x2="12" y2="15"/><line x1="6" y1="6" x2="18" y2="6"/></svg>
</div>
<h3>Graph Output</h3>
<p>Optional property graph (nodes + typed fk/exact_dup/fuzzy_dup/hard_neg edges) via --graph, for graph-based ER and community detection benchmarks.</p>
</div>
</div>
</div>
</section>
<section class="splink-demo" id="demo">
<div class="container reveal">
<h2>Real-World Demo: Splink Benchmark</h2>
<p class="sub">KYC domain, 10K records, hell difficulty — comparing DupeHell's theoretical max against a real Splink pipeline.</p>
<div class="splink-box">
<div class="splink-numbers">
<div class="splink-num">
<div class="label">F1 Achieved</div>
<div class="value achieved">0.410</div>
</div>
<div class="splink-divider"></div>
<div class="splink-num">
<div class="label">F1 Theoretical Max</div>
<div class="value max">0.826</div>
</div>
</div>
<div class="splink-ratio">
<div class="ratio-bar">
<div class="fill" id="ratioFill"></div>
</div>
<div class="ratio-text">
<span>49.6% of theoretical max</span>
<span>Gap: 0.416</span>
</div>
</div>
<div style="margin-top:20px;padding-top:20px;border-top:1px solid var(--border)">
<h4 style="font-family:var(--fm);font-size:.8rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px">Splink Configuration</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;font-size:.82rem">
<div><span style="color:var(--muted)">Algorithm:</span> <span style="color:var(--code)">Dedupe (EM training)</span></div>
<div><span style="color:var(--muted)">Blocking rules:</span> <span style="color:var(--code)">first_name + dob</span></div>
<div><span style="color:var(--muted)">name:</span> <span style="color:var(--code)">jaro_winkler, threshold 0.88</span></div>
<div><span style="color:var(--muted)">address:</span> <span style="color:var(--code)">jaro_winkler, threshold 0.85</span></div>
<div><span style="color:var(--muted)">email:</span> <span style="color:var(--code)">exact match</span></div>
<div><span style="color:var(--muted)">phone / ssn / tax_id:</span> <span style="color:var(--code)">exact match</span></div>
</div>
</div>
<div class="splink-link" style="margin-top:16px">
<a href="demo/splink_benchmark.py">→ View the benchmark script</a>
</div>
<div style="margin-top:24px;padding-top:20px;border-top:1px solid var(--border)">
<p class="sub">
<em>This is a quick, out-of-the-box run with default Splink settings — not an attempt
to find the best possible score. The gap to the theoretical max mostly comes from blocking
rules missing noisy records and heavy name corruption in "hell" mode. On lighter difficulties,
Splink gets much closer to the ceiling.</em>
</p>
</div>
</div>
</div>
</section>
<section class="ethics-banner" id="ethics">
<div class="container reveal">
<div class="inner">
<svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<span class="txt"><strong>Educational & Research Use Only</strong> — This tool is designed for benchmarking and educational purposes. See <a href="ETHICS.md">ETHICS.md</a> for prohibited uses and licensing restrictions.</span>
</div>
</div>
</section>
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div>
<h4>DupeHell</h4>
<p style="color:var(--muted);font-size:.82rem;line-height:1.6;margin-bottom:12px">Synthetic multi-domain dataset generator for record linkage and entity resolution benchmarking.</p>
<p style="color:var(--muted);font-size:.78rem">© 2026 · MIT License</p>
</div>
<div>
<h4>Links</h4>
<ul class="footer-links">
<li><a href="https://github.com/vntoinekaio/DupeHell" target="_blank">GitHub</a></li>
<li><a href="docs/API.md">API Docs</a></li>
<li><a href="docs/GETTING_STARTED.md">Getting Started</a></li>
</ul>
</div>
<div>
<h4>Citation</h4>
<div class="bibtex">@misc{dupehell2026,
title = {DupeHell: Synthetic Multi-Domain
Data for Entity Resolution},
author = {DupeHell Contributors},
year = {2026},
url = {https://github.com/vntoinekaio/DupeHell}
}</div>
</div>
</div>
</div>
</footer>
<script>
(function(){
'use strict';
var copyBtn = document.querySelector('.copy-btn');
if (copyBtn) {
copyBtn.addEventListener('click', function() {
var text = this.getAttribute('data-copy');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
flashCopied(copyBtn);
}).catch(function() {
fallbackCopy(text, copyBtn);
});
} else {
fallbackCopy(text, copyBtn);
}
});
}
function fallbackCopy(text, btn) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
flashCopied(btn);
} catch(e) {}
document.body.removeChild(ta);
}
function flashCopied(btn) {
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(function() {
btn.textContent = 'Copy';
btn.classList.remove('copied');
}, 2000);
}
var reveals = document.querySelectorAll('.reveal');
if ('IntersectionObserver' in window) {
var ro = new IntersectionObserver(function(entries) {
entries.forEach(function(e) {
if (e.isIntersecting) {
e.target.classList.add('visible');
ro.unobserve(e.target);
}
});
}, { threshold: 0.1 });
reveals.forEach(function(el) { ro.observe(el); });
} else {
reveals.forEach(function(el) { el.classList.add('visible'); });
}
var ratioFill = document.getElementById('ratioFill');
var splinkBox = document.querySelector('.splink-box');
if (ratioFill && splinkBox && 'IntersectionObserver' in window) {
var sr = new IntersectionObserver(function(entries) {
entries.forEach(function(e) {
if (e.isIntersecting) {
ratioFill.classList.add('animated');
sr.unobserve(e.target);
}
});
}, { threshold: 0.3 });
sr.observe(splinkBox);
} else if (ratioFill) {
ratioFill.classList.add('animated');
}
})();
</script>
</body>
</html>