# ShiftOps Team Estimation - Current Logic Documentation


**Last Updated:** 2025-11-20

## Overview

ShiftOps currently has **three different implementations** of team size estimation:

1. **ShiftOpsAnalyzer::estimateTeamSize()** - Simple sqrt-based formula (v2/api/shiftops.php:2018)
2. **ShiftOpsCostCalculator::estimateTeamSize()** - Multi-factor weighted approach (v2/api/shiftops-cost-calculator.php:91)
3. **estimateTeamSizeFromReviews()** - JavaScript fallback (v2/pages/shiftops-report.php:6867)

## Implementation 1: ShiftOpsAnalyzer (Multi-Factor Weighted)

**Location:** `v2/api/shiftops.php`, line 2047  
**Return Type:** Integer (team size only)  
**Used In:** `analyzeOnlinePresence()` → `online_presence.team_size_estimate`

### Formula

```
// Multi-factor weighted approach (matches Cost Calculator)
// Factor 1: Customer Volume Proxy (35% weight)
estimatedMonthsOperating = estimateBusinessAge(reviewCount)
reviewsPerEmployeePerMonth = getIndustryBenchmark(primaryType)
volumeFactor = reviewCount / (reviewsPerEmployeePerMonth * estimatedMonthsOperating)
// Cap volume factor: 4.0x for restaurants with 2000+ reviews, 6.0x otherwise
maxVolumeCap = (reviewCount > 2000 && primaryType === 'restaurant') ? 4.0 : 6.0
volumeFactor = clamp(volumeFactor, 0.3, maxVolumeCap)

// Factor 2: Operating Hours (25% weight)
weeklyHours = analyzeBusinessHours(businessData)
hoursFactor = weeklyHours / 40
hoursFactor = clamp(hoursFactor, 0.4, 3.0)

// Factor 3: Service Complexity (20% weight)
serviceTypes = count(available service options)
complexityFactor = 1.0 + (serviceTypes * 0.12)
complexityFactor = clamp(complexityFactor, 1.0, 2.5)

// Factor 4: Quality/Scale Indicator (15% weight)
qualityFactor = calculateQualityFactor(rating, priceLevel)

// Factor 5: Review Velocity (5% weight)
velocityFactor = calculateReviewVelocityFactor(reviewCount, estimatedMonthsOperating)

// Base staffing by industry (scaled based on review count)
baseTeam = getBaseStaffing(primaryType, reviewCount)

// Weighted calculation
estimatedTeam = baseTeam * (
    (volumeFactor * 0.35) +
    (hoursFactor * 0.25) +
    (complexityFactor * 0.20) +
    (qualityFactor * 0.15) +
    (velocityFactor * 0.05)
)

// Location-based adjustments
locationMultiplier = calculateLocationMultiplier(businessData)
estimatedTeam = estimatedTeam * locationMultiplier

// Apply validation bounds
bounds = getValidationBounds(primaryType, reviewCount)
estimatedTeam = max(bounds.min, min(estimatedTeam, bounds.max))

// Safety check: Ensure restaurants don't exceed 25
if (primaryType === 'restaurant' && estimatedTeam > 25):
    estimatedTeam = 25

teamSize = round(estimatedTeam)
```

### Industry Multipliers

| Industry   | Multiplier | Rationale                 |
| ---------- | ---------- | ------------------------- |
| restaurant | 0.8        | ~0.8 staff per 10 reviews |
| cafe       | 0.6        | Lower turnover, regulars  |
| bar        | 0.7        | Moderate staffing needs   |
| store      | 0.5        | Lower review rate         |
| hospital   | 2.0        | High patient volume       |
| pharmacy   | 1.2        | Moderate healthcare needs |
| general    | 0.7        | Default fallback          |

### Factors Used

1. **Review Count** (primary) - Square root scaling
2. **Industry Type** - Multiplier based on business type
3. **Rating** - Adjustment for high/low performers
4. **Service Complexity** - Count of service options

### Validation Bounds

**Restaurant:**

- Min: 5
- Max: `min(25, max(5, ceil(reviewCount / 15)))` - **Capped at 25**

**Cafe:**

- Min: 3
- Max: `min(15, max(3, ceil(reviewCount / 20)))` - **Capped at 15**

**Bar:**

- Min: 3
- Max: `min(25, max(3, ceil(reviewCount / 18)))` - **Capped at 25**

**Store:**

- Min: 2
- Max: `min(30, max(2, ceil(reviewCount / 25)))` - **Capped at 30**

**Hospital:**

- Min: 8
- Max: `min(60, max(8, ceil(reviewCount / 10)))` - **Capped at 60**

**Pharmacy:**

- Min: 3
- Max: `min(20, max(3, ceil(reviewCount / 15)))` - **Capped at 20**

**General:**

- Min: 3
- Max: `min(30, max(3, ceil(reviewCount / 20)))` - **Capped at 30**

### Safety Checks

- **Additional validation**: After location multipliers, ensure restaurants don't exceed 25
- **Applied in all implementations**: Prevents location multipliers from inflating values unrealistically

### Edge Cases

- Minimum team size: Based on industry base staffing (see validation bounds)
- Maximum cap: Industry-specific caps applied (see validation bounds)
- Missing data defaults: reviewCount=0, rating=3.5, priceLevel=2, weeklyHours=40

### Limitations

- No operating hours consideration
- No price level consideration
- No confidence scoring
- No data quality assessment
- Simple rating adjustment (only two thresholds)

---

## Implementation 2: ShiftOpsCostCalculator (Multi-Factor Weighted)

**Location:** `v2/api/shiftops-cost-calculator.php`, line 110  
**Return Type:** Array with detailed metadata  
**Used In:** `calculateCostSavings()` → `cost_savings.team_size_estimate`

### Formula

```
// Factor 1: Customer Volume Proxy (35% weight)
estimatedMonthsOperating = estimateBusinessAge(reviewCount)
reviewsPerEmployeePerMonth = getIndustryBenchmark(primaryType)
volumeFactor = reviewCount / (reviewsPerEmployeePerMonth * estimatedMonthsOperating)
// Cap volume factor: 4.0x for restaurants with 2000+ reviews, 6.0x otherwise
maxVolumeCap = (reviewCount > 2000 && primaryType === 'restaurant') ? 4.0 : 6.0
volumeFactor = clamp(volumeFactor, 0.3, maxVolumeCap)

// Factor 2: Operating Hours (25% weight)
weeklyHours = analyzeBusinessHours(businessData)
hoursFactor = weeklyHours / 40
hoursFactor = clamp(hoursFactor, 0.4, 3.0)

// Factor 3: Service Complexity (20% weight)
serviceTypes = count(available service options)
complexityFactor = 1.0 + (serviceTypes * 0.12)
complexityFactor = clamp(complexityFactor, 1.0, 2.5)

// Factor 4: Quality/Scale Indicator (15% weight)
qualityFactor = calculateQualityFactor(rating, priceLevel)

// Factor 5: Review Velocity (5% weight)
velocityFactor = calculateReviewVelocityFactor(reviewCount, estimatedMonthsOperating)

// Base staffing by industry (scaled based on review count)
baseTeam = getBaseStaffing(primaryType, reviewCount)

// Weighted calculation
estimatedTeam = baseTeam * (
    (volumeFactor * 0.35) +
    (hoursFactor * 0.25) +
    (complexityFactor * 0.20) +
    (qualityFactor * 0.15) +
    (velocityFactor * 0.05)
)

// Location-based adjustments
locationMultiplier = calculateLocationMultiplier(businessData)
estimatedTeam = estimatedTeam * locationMultiplier

// Apply validation bounds
bounds = getValidationBounds(primaryType, reviewCount)
estimatedTeam = max(bounds.min, min(estimatedTeam, bounds.max))

// Safety check: Ensure restaurants don't exceed 25
if (primaryType === 'restaurant' && estimatedTeam > 25):
    estimatedTeam = 25

estimatedTeam = round(estimatedTeam)
```

### Industry Benchmarks

#### Reviews Per Employee Per Month

| Industry   | Reviews/Employee/Month | Rationale                                    |
| ---------- | ---------------------- | -------------------------------------------- |
| restaurant | 10                     | Busy restaurant ~8-12 reviews/employee/month |
| cafe       | 6                      | Lower turnover, regulars                     |
| bar        | 8                      | Moderate review rate                         |
| store      | 4                      | Lower review rate                            |
| hospital   | 12                     | High patient volume                          |
| pharmacy   | 7                      | Moderate healthcare review rate              |
| general    | 6                      | Default fallback                             |

#### Base Staffing (Minimum Operational Team)

| Industry   | Base Staff | Rationale                        |
| ---------- | ---------- | -------------------------------- |
| restaurant | 5          | Chef, 2 cooks, 2 servers minimum |
| cafe       | 3          | 2 baristas, 1 cashier            |
| bar        | 3          | 2 bartenders, 1 server           |
| store      | 2          | Cashier, stock person            |
| hospital   | 8          | Medical staff requirements       |
| pharmacy   | 3          | Pharmacist, 2 techs              |
| general    | 3          | Default fallback                 |

#### Business Age Estimation

| Review Count | Estimated Months Operating |
| ------------ | -------------------------- |
| > 1000       | 60 months (5 years)        |
| > 500        | 48 months (4 years)        |
| < 50         | 12 months (1 year)         |
| default      | 36 months (3 years)        |

### Factor Weights

| Factor             | Weight | Description                                |
| ------------------ | ------ | ------------------------------------------ |
| Customer Volume    | 35%    | Reviews per employee per month calculation |
| Operating Hours    | 25%    | Weekly hours / 40 (standard full-time)     |
| Service Complexity | 20%    | +12% per service type                      |
| Quality/Scale      | 15%    | Rating + price level combination           |
| Review Velocity    | 5%     | Review velocity factor                     |

### Return Structure

```php
[
    'team_size' => int,
    'confidence_level' => 'low'|'medium'|'high',
    'factors_used' => [
        'volume' => float,
        'hours' => float,
        'complexity' => float,
        'quality' => float
    ],
    'base_staffing' => int,
    'data_quality' => array
]
```

### Confidence Calculation

**Method:** `calculateEstimationConfidence()` (line 506)

```
score = 0
if (reviewCount > 50): score += 2
if (has opening_hours): score += 2
if (has service_options): score += 1
if (has price_level): score += 1

if (score >= 5): return 'high'
if (score >= 3): return 'medium'
return 'low'
```

### Data Quality Assessment

**Method:** `assessDataQuality()` (line 520)

Returns array of quality indicators:

- `'high_review_volume'` - if reviewCount > 100
- `'complete_hours'` - if opening_hours present
- `'service_details'` - if service_options present
- `'pricing_info'` - if price_level present

### Validation Bounds

**Restaurant:**

- Min: 5
- Max: `min(25, max(5, ceil(reviewCount / 15)))` - **Capped at 25**

**Cafe:**

- Min: 3
- Max: `min(15, max(3, ceil(reviewCount / 20)))` - **Capped at 15**

**Bar:**

- Min: 3
- Max: `min(25, max(3, ceil(reviewCount / 18)))` - **Capped at 25**

**Store:**

- Min: 2
- Max: `min(30, max(2, ceil(reviewCount / 25)))` - **Capped at 30**

**Hospital:**

- Min: 8
- Max: `min(60, max(8, ceil(reviewCount / 10)))` - **Capped at 60**

**Pharmacy:**

- Min: 3
- Max: `min(20, max(3, ceil(reviewCount / 15)))` - **Capped at 20**

**General:**

- Min: 3
- Max: `min(30, max(3, ceil(reviewCount / 20)))` - **Capped at 30**

### Safety Checks

- **Additional validation**: After location multipliers, ensure restaurants don't exceed 25
- **Applied in all implementations**: Prevents location multipliers from inflating values unrealistically

### Edge Cases

- Minimum team size: Based on industry base staffing (see validation bounds)
- Maximum cap: Industry-specific caps applied (see validation bounds)
- Factor clamping: All factors have min/max bounds
- Missing data defaults: reviewCount=0, rating=3.5, priceLevel=2, weeklyHours=40

### Limitations

- Business age estimation is rough heuristic
- Reviews per employee per month is industry average, not business-specific
- Operating hours analysis depends on Google Places data quality
- No consideration of location type (urban/suburban/rural)
- No consideration of seasonal variations

---

## Implementation 3: JavaScript Fallback (Report Page)

**Location:** `v2/pages/shiftops-report.php`, line 7288  
**Return Type:** Object with metadata  
**Used In:** Client-side fallback when API data unavailable

### Formula

```javascript
// Multi-factor weighted approach (matches PHP implementations)
// Factor 1: Customer Volume Proxy (35% weight)
estimatedMonthsOperating = estimateBusinessAge(reviewCount);
reviewsPerEmployeePerMonth = getIndustryBenchmark(primaryType);
volumeFactor = reviewCount / (reviewsPerEmployeePerMonth * estimatedMonthsOperating);
// Cap volume factor: 4.0x for restaurants with 2000+ reviews, 6.0x otherwise
maxVolumeCap = (reviewCount > 2000 && primaryType === 'restaurant') ? 4.0 : 6.0;
volumeFactor = Math.max(0.3, Math.min(maxVolumeCap, volumeFactor));

// Factor 2: Operating Hours (25% weight)
weeklyHours = analyzeBusinessHours(businessData);
hoursFactor = weeklyHours / 40;
hoursFactor = Math.max(0.4, Math.min(3.0, hoursFactor));

// Factor 3: Service Complexity (20% weight)
serviceTypes = count(available service options);
complexityFactor = 1.0 + (serviceTypes * 0.12);
complexityFactor = Math.max(1.0, Math.min(2.5, complexityFactor));

// Factor 4: Quality/Scale Indicator (15% weight)
qualityFactor = calculateQualityFactor(rating, priceLevel);

// Factor 5: Review Velocity (5% weight)
velocityFactor = calculateReviewVelocityFactor(reviewCount, estimatedMonthsOperating);

// Base staffing by industry (scaled based on review count)
baseTeam = getBaseStaffing(primaryType, reviewCount);

// Weighted calculation
estimatedTeam = baseTeam * (
    (volumeFactor * 0.35) +
    (hoursFactor * 0.25) +
    (complexityFactor * 0.20) +
    (qualityFactor * 0.15) +
    (velocityFactor * 0.05)
);

// Location-based adjustments
locationMultiplier = calculateLocationMultiplier(businessData);
estimatedTeam = estimatedTeam * locationMultiplier;

// Apply validation bounds
bounds = getValidationBounds(primaryType, reviewCount);
estimatedTeam = Math.max(bounds.min, Math.min(estimatedTeam, bounds.max));

// Safety check: Ensure restaurants don't exceed 25
if (primaryType === 'restaurant' && estimatedTeam > 25) {
    estimatedTeam = 25;
}

estimatedTeamSize = Math.round(estimatedTeam);
```

### Industry Multipliers

**EXACT SAME VALUES as Implementation 1:**

| Industry   | Multiplier |
| ---------- | ---------- |
| restaurant | 0.8        |
| cafe       | 0.6        |
| bar        | 0.7        |
| store      | 0.5        |
| hospital   | 2.0        |
| pharmacy   | 1.2        |
| general    | 0.7        |

### Differences from PHP Implementation 1

- **Missing:** Rating adjustment
- **Missing:** Service complexity adjustment
- **Has:** Confidence calculation (simplified)

### Confidence Calculation

```javascript
if (reviewCount > 100): confidence = 'high'
else if (reviewCount > 50): confidence = 'medium'
else: confidence = 'low'
```

### Return Structure

```javascript
{
    estimated_team_size: int,
    confidence: 'low'|'medium'|'high',
    primary_type: string,
    multiplier: float
}
```

### Validation Bounds

**Same as PHP implementations** - See Implementation 2 validation bounds section above.

### Safety Checks

- **Additional validation**: After location multipliers, ensure restaurants don't exceed 25
- **Console logging**: Logs when validation bounds are applied for debugging

### Limitations

- Requires business data for accurate calculation (falls back to simplified calculation if missing)
- Console logging only available in browser environment

---

## Comparison Matrix

| Feature                 | Analyzer                                 | Cost Calculator                          | JavaScript                               |
| ----------------------- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- |
| **Formula Base**        | baseTeam \* weighted factors             | baseTeam \* weighted factors             | baseTeam \* weighted factors             |
| **Review Count**        | ✓ Factor 1 (35%)                         | ✓ Factor 1 (35%)                         | ✓ Factor 1 (35%)                         |
| **Industry Type**       | ✓ Base staffing + reviews/emp            | ✓ Base staffing + reviews/emp            | ✓ Base staffing + reviews/emp            |
| **Rating**              | ✓ Factor 4 (15%)                         | ✓ Factor 4 (15%)                         | ✓ Factor 4 (15%)                         |
| **Service Complexity**  | ✓ Factor 3 (20%)                         | ✓ Factor 3 (20%)                         | ✓ Factor 3 (20%)                         |
| **Operating Hours**     | ✓ Factor 2 (25%)                         | ✓ Factor 2 (25%)                         | ✓ Factor 2 (25%)                         |
| **Price Level**         | ✓ Factor 4 (15%)                         | ✓ Factor 4 (15%)                         | ✓ Factor 4 (15%)                         |
| **Location Multiplier** | ✓ Applied                                | ✓ Applied                                | ✓ Applied                                |
| **Confidence Score**    | ✓ Calculated                             | ✓ Calculated                             | ✓ Calculated                             |
| **Data Quality**        | ✓ Assessed                               | ✓ Assessed                               | ✓ Assessed                               |
| **Max Cap**             | ✓ Industry-specific (25 for restaurants) | ✓ Industry-specific (25 for restaurants) | ✓ Industry-specific (25 for restaurants) |
| **Min Cap**             | ✓ Industry-specific                      | ✓ Industry-specific                      | ✓ Industry-specific                      |
| **Safety Checks**       | ✓ Restaurant cap at 25                   | ✓ Restaurant cap at 25                   | ✓ Restaurant cap at 25                   |
| **Validation Logging**  | ✗ Missing                                | ✗ Missing                                | ✓ Console logging                        |
| **Return Type**         | int                                      | array                                    | object                                   |

## Data Flow

### Analyzer Path

```
ShiftOpsAnalyzer::analyzeBusiness()
  → analyzeOnlinePresence()
    → estimateTeamSize()  [v2/api/shiftops.php:2047]
      → Returns: int
      → Stored in: analysis_data.online_presence.team_size_estimate
```

### Cost Calculator Path

```
ShiftOpsAnalyzer::analyzeBusiness()
  → calculateCostSavings()
    → ShiftOpsCostCalculator::calculateCostSavings()
      → estimateTeamSize()  [v2/api/shiftops-cost-calculator.php:110]
        → Returns: array
        → Stored in: analysis_data.cost_savings.team_size_estimate
```

### JavaScript Fallback Path

```
shiftops-report.php (client-side)
  → estimateTeamSizeFromReviews()  [v2/pages/shiftops-report.php:7288]
    → Returns: object
    → Used when: API data unavailable

shiftops.php (client-side, loading screen)
  → estimateTeamSizeEnhanced()  [v2/pages/shiftops.php:1949]
    → Returns: integer
    → Used when: API data unavailable
```

### Data Source Priority (Loading Screen & Report Page)

**Both pages now use same priority order:**

1. `cost_savings.team_size_estimate` (from API)
2. `online_presence.team_size_estimate` (from API)
3. Fallback calculation (same function with same parameters)

**This ensures consistent values between loading screen and report page.**

## Consistency Achieved

All implementations now use:

1. **Same formula:** Multi-factor weighted approach with same factor weights (35%, 25%, 20%, 15%, 5%)
2. **Same factors:** All implementations include volume, hours, complexity, quality, and velocity factors
3. **Same validation bounds:** Industry-specific caps applied consistently (restaurant: 25, cafe: 15, etc.)
4. **Same safety checks:** Additional validation to ensure restaurants don't exceed 25
5. **Same volume factor caps:** 4.0x for restaurants with 2000+ reviews, 6.0x otherwise
6. **Same data source priority:** Loading screen and report page prioritize API values consistently
7. **Same location multipliers:** Applied consistently across all implementations

**Note:** Return types differ (int vs array vs object) but calculation logic is identical.

## Hardcoded Values

### Analyzer

- Industry multipliers: 0.8, 0.6, 0.7, 0.5, 2.0, 1.2, 0.7
- Rating thresholds: 4.5 (high), 3.5 (low)
- Service complexity multipliers: 1.3 (3+), 1.1 (2+)
- Rating adjustments: 1.2 (high), 0.8 (low)
- Minimum: 1

### Cost Calculator (and all implementations)

- Reviews per employee/month: 10, 6, 8, 4, 12, 7, 6
- Base staffing: Scaled by review count (see base staffing ranges)
- Factor weights: 0.35, 0.25, 0.20, 0.15, 0.05
- Factor caps: volume (0.3-4.0 for restaurants with 2000+ reviews, 0.3-6.0 otherwise), hours (0.4-3.0), complexity (1.0-2.5)
- Business age: 12, 24, 36, 48, 60 months
- Review thresholds: 25, 100, 500, 1000
- Max cap formula: Industry-specific (restaurant: min(25, max(5, ceil(reviews/15))))
- Minimum: Industry-specific (restaurant: 5, cafe: 3, etc.)
- Safety checks: Restaurant cap at 25 after all calculations

### JavaScript (Report & Loading Screen)

- Same as Cost Calculator (all implementations use identical logic)
- Validation logging: Console logs when bounds are applied
- Minimum: Industry-specific (same as Cost Calculator)

## Assumptions

1. **Review count correlates with team size** - More reviews = more staff (primary assumption)
2. **Multi-factor weighted approach** - Multiple factors contribute to team size estimate
3. **Industry averages** - Benchmarks apply to all businesses in category
4. **Business age** - Estimated from review count (all implementations)
5. **Standard workweek** - 40 hours per full-time employee (all implementations)
6. **Service complexity** - More services = more staff needed (+12% per service type)
7. **Quality indicators** - Higher rating/price = more staff
8. **Location matters** - Urban locations and high competitor density require more staff
9. **Realistic caps** - Single-location restaurants max at 25 employees
10. **Volume factor limits** - Restaurants with 2000+ reviews capped at 4.0x to prevent unrealistic inflation

## Edge Cases Handled

- Zero reviews → Minimum team size (industry-specific, e.g., restaurant: 5)
- Missing business type → Default to 'general'
- Missing rating → Default to 3.5 (all implementations)
- Missing service options → Count = 0
- Missing operating hours → Default to 40 hours/week (all implementations)
- Missing price level → Default to 2 (all implementations)
- Extreme review counts → Capped by industry-specific max formula (all implementations)
- Location multipliers pushing values too high → Safety check ensures restaurants don't exceed 25
- Very high review counts (2000+) → Volume factor cap reduced to 4.0x for restaurants

## Known Limitations

1. **Location consideration** - Urban vs suburban vs rural factored via multipliers, but may not capture all nuances
2. **No seasonal variation** - Same estimate year-round
3. **No actual capacity data** - Seating capacity, square footage not used
4. **No multi-location detection** - Chains treated same as single locations
5. **Rough business age estimation** - Based only on review count
6. **Industry averages** - May not reflect actual business size
7. **Review rate assumptions** - Reviews per employee per month is estimated
8. **No validation against real data** - No known accuracy metrics
9. **Validation bounds** - Caps are estimates based on industry research, may need adjustment with real data
10. **Volume factor cap** - 4.0x cap for restaurants with 2000+ reviews is conservative estimate
