DOCUMENTATION

Auto-Tuner Anatomy ③: Grid Search Engine — Optimizing Impact, Dampening, and Silence

Dissecting the core engine that optimizes Impact, Dampening, and Silence Penalty via Grid Search. Phase-based dynamic ranges, separation simulation, and the mathematical structure of Compound Score explained at the code level.

In the previous part: ② Signal Lift Anatomy, we covered signal discriminative power analysis. This part dissects the Auto-Tuner's heart — the Grid Search optimization engine.



1.1 Principle

"Try changing the parameter slightly, and recommend the value that produces the best result."

That is all Grid Search does. No complex math required, intuitively understandable, and the rationale behind results can be clearly explained.

for candidate in search_range:
    separation = simulate(replace current parameter with candidate)
    if separation > best_record:
        best_record = separation
        optimal_value = candidate
recommendation = optimal_value

1.2 Why Not Use More Advanced Methods?

More sophisticated methods exist — Gradient Descent, Bayesian Optimization, Genetic Algorithms, etc. Here's why the Auto-Tuner sticks with Grid Search:

CriterionGrid SearchGradient Descent
Explainability✅ "Maximum separation at this value"❌ "The value where the algorithm converged"
Global optimum guarantee✅ Exhaustive within range❌ Can get stuck in local optima
Differentiability required❌ Not needed✅ Required
Speed ((N+2)-dim)⚠️ Sequential 1-D✅ Simultaneous optimization

The Auto-Tuner performs sequential 1-D optimization (coordinate descent). It ignores interaction effects, but each recommendation's rationale can be individually explained.

MCMC (Part 6) compensates for this limitation — simultaneous (N+2)-dimensional estimation (NN = number of Impact types excluding No Signal).



2. Impact Optimization

2.1 Purpose

Impact is the weight of a signal. Game Changer's Impact = 5.0 means one Game Changer signal adds 5.0 to α.

Is 5.0 really optimal? Perhaps 4.0 yields higher separation, or 6.0 might be better.

2.2 Search Range: Phase-Based Dynamic Expansion

When data is scarce, searching a wide range risks overfitting. Range expands as Phase increases:

PhaseRangeExample (current=5.0)Rationale
2±20%4.0 ~ 6.0Small data, conservative
3±30%3.5 ~ 6.5Default range
4±40%3.0 ~ 7.0Sufficient data
5±50%2.5 ~ 7.5Wide exploration
GRID_RANGE_RATIO_BY_PHASE = {
  1 => BASE_GRID_RANGE_RATIO,      # 0.20
  2 => BASE_GRID_RANGE_RATIO,      # 0.20
  3 => 0.30,
  4 => 0.40,
  5 => 0.50
}.freeze

2.3 Grid Resolution

11 points are evenly distributed:

current=5.0, range=±30%[3.5, 3.8, 4.1, 4.4, 4.7, 5.0, 5.3, 5.6, 5.9, 6.2, 6.5]

Why 11 points? A balance between resolution and speed. 5–6 points are too coarse, 50 would take 50× longer. 11 provides sufficient precision while running fast.

2.4 Single Simulation

For one candidate value:
def optimize_impact(impact_type, current_value)
  range = current_value * grid_range_ratio  # Phase-based
  candidates = (0..GRID_POINTS).map { |i|
    low + (high - low) * i / GRID_POINTS
  }

  best_val = current_value
  best_sep = current_separation[:separation]

  candidates.each do |candidate|
    override = { impact_type.id => candidate }
    sep = simulate_separation(impact_override: override)
    if sep > best_sep
      best_sep = sep
      best_val = candidate
    end
  end

  { current: current_value, recommended: best_val,
    improvement: best_sep - current_separation[:separation] }
end

simulate_separation re-simulates all Won/Lost projects from scratch. A single call performs computations proportional to project count × activity count.

2.5 Minimum Improvement Threshold

If separation improvement is less than MIN_SEPARATION_IMPROVEMENT = 0.01, the current value is retained.

Why? An improvement of 0.003 is likely statistical noise. It could vanish with one more data point.

action: improvement >= MIN_SEPARATION_IMPROVEMENT ? 'adjust' : 'keep'


3. Compound Score: The Mathematics of Dampening

3.1 The Problem

Three signals emerged from a single meeting:

  • Game Changer (5.0)
  • Strong Affirmation (1.0)
  • Moderate Affirmation (0.7)

Adding all three gives 6.7. But this is potentially redundant information from the same context. The customer was highly positive, causing all three signals to be observed — not three independent positive events.

3.2 Solution: MAX + Remaining × Dampening

def compound_with_dampening(scores, dampening)
  return 0.0 if scores.empty?
  sorted = scores.sort.reverse
  # Strongest signal reflected at 100%
  # Remaining reflected at dampening ratio
  sorted[0] + sorted[1..].sum * dampening
end

With current dampening = 0.25:

Compound = 5.0 + (1.0 + 0.7) × 0.25
         = 5.0 + 0.425
         = 5.425
  • dampening = 0: Only 5.0 is reflected (rest ignored)
  • dampening = 0.25: 5.425 (slight reflection)
  • dampening = 1.0: 6.7 (full reflection)

3.3 Why Separate MAX First?

The strongest signal is the dominant indicator. It should be fully reflected. The rest are confirmatory supplements and are discounted. This aligns with the information theory concept of marginal information.

def optimize_dampening
  candidates = (0..GRID_POINTS).map { |i| i.to_f / GRID_POINTS }  # [0.0, 0.09, ..., 1.0]

  best_dampening = BayesianUpdate::SIGNAL_DAMPENING
  best_sep = current_separation[:separation]

  candidates.each do |d|
    sep = simulate_separation(dampening_override: d)
    if sep > best_sep
      best_sep = sep
      best_dampening = d
    end
  end

  { current: BayesianUpdate::SIGNAL_DAMPENING,
    recommended: best_dampening,
    improvement: best_sep - current_separation[:separation] }
end


4. Silence Penalty Optimization

4.1 The Penalty of Silence

If the customer hasn't been contacted for a certain period (default 14 days), a penalty is added to β:

β+=unit_penalty×count\beta \mathrel{+}= \text{unit\_penalty} \times \text{count}

Where:

  • unit_penalty = Weak Negation default value × silence_ratio
  • count = (elapsed days − gap) / interval + 1

4.2 Meaning of silence_ratio

If silence_ratio = 0.3 and Weak Negation = 0.4:

unit_penalty = 0.4 × 0.3 = 0.12

If the 14-day gap is exceeded by 3 intervals of 7 days:

β += 0.12 × 3 = 0.36

This provides the intuition: "equivalent to receiving 3 weak negative signals."

Silence ratio is also searched across 11 points in the 0.0–1.0 range:

[0.0, 0.09, 0.18, ..., 0.91, 1.0]

silence_ratio = 0 means no silence penalty; 1.0 imposes the full Weak Negation as penalty.



5. Simulation Engine Internals

5.1 simulate_separation Full Flow

simulate_separation(impact_override, dampening_override, silence_ratio_override)
  ├─ for each Won project:
  │    └─ p_win = simulate_project(project, overrides)
  ├─ for each Lost project:
  │    └─ p_win = simulate_project(project, overrides)
  ├─ won_avg = mean(Won p_win values)
  ├─ lost_avg = mean(Lost p_win values)
  └─ return won_avg - lost_avg

5.2 simulate_project Step by Step

def simulate_project(project, impact_override, dampening, silence_ratio)
  alpha = project.prior_alpha.to_f    # ── ① Prior initial value
  beta  = project.prior_beta.to_f

  activities.each do |activity|        # ── ② Chronological activity traversal
    swv = stage.swv.to_f              # ── ③ Stage weight

    # ── ④ Compound Score calculation
    positive_compound, negative_compound =
      simulate_compound_scores_fast(signal_ids, impacts, impact_override, dampening)

    # ── ⑤ Silence penalty
    if elapsed >= gap
      count = ((elapsed - gap) / interval) + 1
      beta += unit_penalty * count
    end

    # ── ⑥ α, β update
    alpha += swv * positive_compound
    beta  += swv * negative_compound
  end

  alpha / (alpha + beta)              # ── ⑦ Return P(Win)
end

① → ② → ③ → ④ → ⑤ → ⑥ → ⑦ — This is the complete lifecycle calculation for 1 project in EXAWin's Bayesian engine.

5.3 SWV (Stage Weight Value)

SWV is the per-stage weight. A signal at the Proposal stage is more decisive than one at Discovery. Higher SWV means the same signal has a larger impact on α/β.



6. Performance: Why It's Fast

The Auto-Tuner's Grid Search requires at most:

ImpactType count × GRID_POINTS × Project count × Activity count
= 9 × 11 × 100 × 20 = 198,000 operations

Adding Dampening (11 iterations) and Silence (11 iterations) simulations totals approximately 220,000 operations.

This is feasible because:

  1. Preloading: All data is in memory (0 DB queries)
  2. Simple arithmetic: Only multiplication, addition, and comparison (no matrix operations)
  3. 1-D search: An N-dimensional Grid would be 11N11^N, but sequential 1-D is 11×N11 \times N

220,000 arithmetic operations in Ruby completes in under 1 second.



Next: ④ Threshold · k Anatomy — Youden J statistic, T optimization, k Grid Search.