Back to blogs
Restaurant Analytics
8 min read
Jan 20, 2025

Designing Fair Performance Metrics: A Points System That Distributes Fixed Bonus Pools Equitably

How PerformancePilot translates business requirements—punctuality, referrals, tenure—into a weighted mathematical model that distributes a capped monthly bonus pool equitably across restaurant staff, including edge cases for new hires and part-time workers.

Points SystemPerformance MetricsAnalyticsAlgorithm DesignSaaSNode.jsPostgreSQL
Table of contents

When a restaurant group with 120 staff and a fixed ₹2 lakh monthly bonus pool asks you to build a "fair performance scoring system," the word "fair" is doing a lot of work. Fair to whom? On what dimensions? What happens when a new hire with one week of data sits next to a five-year veteran?

This is the scoring system I built for PerformancePilot, and the reasoning behind every design decision.

The Business Requirements

The client was a restaurant group operating six locations. Their management team gave me these requirements for the monthly bonus:

  • Punctuality: Staff who consistently arrive on time should score higher
  • Customer referrals: Staff who generate measurable referrals (tracked via referral codes) should be rewarded
  • Tenure: Long-serving staff should have a baseline advantage to reward loyalty
  • Performance ratings: Managers can give weekly 1-5 star ratings
  • The bonus pool is fixed: ₹2 lakh per month, distributed—not paid as a flat per-person amount

The last requirement is the hardest one. A fixed pool means performance is relative, not absolute. If everyone has a great month, nobody gets a huge bonus—the pie doesn't grow. This is intentional: the restaurant's labor cost stays predictable.

The Scoring Model

I designed a weighted composite score for each staff member:

Final Score = (Punctuality × 0.35) + (Manager Rating × 0.25) + (Referrals × 0.20) + (Tenure × 0.15) + (Shift Completion × 0.05)

The weights were determined with the management team through a structured workshop. They ranked what mattered most (punctuality, then manager rating) and we translated that into weights.

Each dimension is normalized to 0-100 before weighting:

Punctuality Score (0-100)

function punctualityScore(attendanceRecords: AttendanceRecord[]): number {
  if (attendanceRecords.length === 0) return 50; // neutral for new hires
 
  const onTimeCount = attendanceRecords.filter(
    (r) => r.clock_in_delta_minutes <= 5 // allow 5-minute grace
  ).length;
 
  const lateCount = attendanceRecords.filter(
    (r) => r.clock_in_delta_minutes > 5 && r.clock_in_delta_minutes <= 15
  ).length;
 
  const veryLateCount = attendanceRecords.filter(
    (r) => r.clock_in_delta_minutes > 15
  ).length;
 
  const totalShifts = attendanceRecords.length;
 
  // Weighted penalty: very late hurts more than slightly late
  const adjustedScore =
    (onTimeCount * 1.0 + lateCount * 0.5 + veryLateCount * 0) / totalShifts;
 
  return Math.round(adjustedScore * 100);
}

Tenure Score (0-100)

Tenure is logarithmic, not linear. The first year of tenure matters a lot; year 6 vs year 7 matters much less:

function tenureScore(joinDate: Date): number {
  const monthsEmployed = differenceInMonths(new Date(), joinDate);
 
  // Log scale: score grows quickly at first, plateaus for long-tenured staff
  // 1 month → ~20, 6 months → ~48, 1 year → ~60, 3 years → ~80, 5+ years → 100
  const raw = Math.log(monthsEmployed + 1) / Math.log(60 + 1);
  return Math.min(100, Math.round(raw * 100));
}

Using log scale was deliberate: linear tenure scoring would mean a 10-year employee gets 10x the tenure contribution of a 1-year employee. That's neither fair to younger staff nor representative of the management's intent.

Referral Score (0-100)

Referrals are normalized against the top performer in the current period:

function referralScore(
  staffReferrals: number,
  maxReferralsThisPeriod: number
): number {
  if (maxReferralsThisPeriod === 0) return 0;
  return Math.round((staffReferrals / maxReferralsThisPeriod) * 100);
}

Normalizing against the period maximum prevents one superstar from making everyone else's referral score look pathetic. A staff member with 3 referrals in a period where the max is 4 scores 75, not 3% of some absolute benchmark.

Distributing the Fixed Bonus Pool

Once every staff member has a final composite score, the pool distribution is:

function distributeBonus(
  scores: Array<{ staff_id: string; score: number }>,
  poolAmount: number
): Array<{ staff_id: string; bonus: number }> {
  const totalScore = scores.reduce((sum, s) => sum + s.score, 0);
 
  if (totalScore === 0) {
    // Edge case: all zeros—distribute equally
    const equalShare = Math.floor(poolAmount / scores.length);
    return scores.map((s) => ({ staff_id: s.staff_id, bonus: equalShare }));
  }
 
  const rawBonuses = scores.map((s) => ({
    staff_id: s.staff_id,
    bonus: (s.score / totalScore) * poolAmount,
  }));
 
  // Round down to avoid exceeding pool; redistribute remainder
  const flooredBonuses = rawBonuses.map((b) => ({
    ...b,
    bonus: Math.floor(b.bonus),
  }));
 
  const distributed = flooredBonuses.reduce((sum, b) => sum + b.bonus, 0);
  let remainder = poolAmount - distributed;
 
  // Give remainder to highest fractional parts
  const fractionalParts = rawBonuses
    .map((b, i) => ({ index: i, fractional: b.bonus - Math.floor(b.bonus) }))
    .sort((a, b) => b.fractional - a.fractional);
 
  for (const { index } of fractionalParts) {
    if (remainder <= 0) break;
    flooredBonuses[index].bonus += 1;
    remainder--;
  }
 
  return flooredBonuses;
}

The remainder distribution (giving leftover rupees to the highest fractional parts, a variant of Hamilton's method) ensures the pool is fully distributed with no rupee left unallocated, and the rounding favors those who were closest to the next rupee.

Edge Cases That Changed the Design

New Hires with Insufficient Data

A staff member in their first two weeks doesn't have enough data for a meaningful punctuality or manager rating score. Originally, I scored them at 0 on insufficient dimensions—which meant they could never compete for any bonus in their first pay period. Management rejected this as demotivating for new hires.

The fix: neutral scoring (50/100) for any dimension with fewer than 5 data points:

function safeScore(values: number[], calculateFn: (v: number[]) => number): number {
  if (values.length < 5) return 50; // neutral, not penalized
  return calculateFn(values);
}

Part-Time vs Full-Time Staff

Part-time staff work fewer shifts, so raw referral and punctuality counts would naturally be lower. Normalizing by shifts worked rather than absolute counts fixed this:

function shiftNormalizedReferrals(referrals: number, shiftsWorked: number): number {
  if (shiftsWorked === 0) return 0;
  return referrals / shiftsWorked; // referrals per shift, then normalize
}

Manager Gaming the Rating

Early testing revealed that location managers could boost their friends' scores with 5-star weekly ratings and suppress others with 1-stars. We added:

  1. Rating variance check: if a manager's ratings for a staff member show unusual variance (e.g., 5-1-5-1 pattern), the system flags it for review.
  2. Manager weight reduction: the manager rating dimension weight was reduced from 0.35 to 0.25 after observing the gaming behavior in the pilot.
  3. Rating anonymization: staff cannot see their own manager ratings during the month—only the final score.

Results

After three months in production across all six locations:

  • Bonus acceptance rate: 97% of staff agreed the distribution was fair (anonymous survey)
  • Punctuality improvement: late arrivals dropped 31% from the quarter before
  • Manager disputes: two managers were flagged by the variance checker; one case was investigated and found to involve genuine gaming

The system didn't eliminate subjectivity—the weight assignments are inherently a value judgment by management. But it made those values explicit and applied them consistently, which is as fair as a bonus system can get.

Key Takeaways

  • Make the weighting choices explicit and get management to sign off before writing code. Implicit weights cause disputes later.
  • Log scale for tenure prevents veteran employees from dominating scores in a way that feels punitive to newer staff.
  • Normalize competitive dimensions (referrals) against the period maximum, not an absolute target—absolute targets make exceptional performance invisible.
  • Hamilton's method for remainder distribution is the fairest way to round fractional bonus amounts without over- or under-distributing the pool.
  • Neutral scoring for insufficient data is better than zero scoring—don't punish employees for not having history.
  • Add anti-gaming detection from day one—systems that affect pay will always attract attempts to manipulate them.