# Sync — calculation reference

This document describes **numeric and algorithmic formulas** implemented in the codebase. It is meant for engineers auditing behavior; the **source of truth** is always the referenced files.

---

## 1. Influencer engagement aggregates (Instagram data)

**Where:** `backend/influencers/services/data_service.py` — `calculate_averages()`

After media posts are stored, aggregates are computed with Django `Avg`:

| Field | Meaning |
|--------|---------|
| `ave_impressions` | Mean `impressions` over posts |
| `ave_reach` | Mean `reach` over posts |
| `ave_engagement` | Mean Graph API **`engagement`** field per post |
| `avg_likes` | Mean `like_count` |
| `avg_comments` | Mean `comment_count` |
| `ave_saves` | Mean `saved` |
| `ave_video_views` | Mean `video_views` on video posts only (if any) |

**Engagement rate (%), stored as `engagement_rate_percent`:**

\[
\text{engagement\_rate\_percent} = \begin{cases}
\displaystyle \frac{\text{ave\_engagement}}{\text{followers\_count}} \times 100 & \text{if followers} > 0 \\[0.5em]
\text{(unchanged / not set via this path)} & \text{otherwise}
\end{cases}
\]

`ave_engagement` is whatever Meta returns as **engagement** per post (platform-defined aggregate), averaged across posts.

---

## 2. Estimated price per post (hybrid model)

**Where:** `backend/core/utils/pricing.py` — `calculate_estimated_price()`

Outputs **`estimated_min`**, **`estimated_max`**, **`estimated_avg`** (USD-style units as used in code). Influencer profiles persist min/max from this logic (via `NicheService` / tasks).

### 2.1 Follower tier (from counts only)

| Condition | Tier |
|-----------|------|
| followers < 10,000 | Nano |
| 10,000 ≤ followers < 100,000 | Micro |
| 100,000 ≤ followers < 500,000 | Mid |
| 500,000 ≤ followers < 1,000,000 | Macro |
| followers ≥ 1,000,000 | Mega |

### 2.2 Effective tier (follower tier + LLM tier clamp)

Let `calculated_tier` be the tier from §2.1 and `llm_predicted_tier` the top tier from the ML service (must be one of `Nano, Micro, Mid, Macro, Mega`).

Indices follow `TIER_ORDER = ["Nano", "Micro", "Mid", "Macro", "Mega"]`.

If `llm_predicted_tier` is valid:

\[
\text{effective\_tier\_idx} = \max\bigl(\text{calc\_idx} - 1,\; \min(\text{calc\_idx} + 1,\; \text{llm\_idx})\bigr)
\]

Then `effective_tier = TIER_ORDER[effective_tier_idx]`.

If LLM tier is missing/invalid, `effective_tier = calculated_tier`.

**Intent:** Never let the LLM move pricing more than **one tier** away from the follower-based tier (reduces catastrophic mispricing).

### 2.3 Base rate per 1K followers

From `effective_tier`:

| effective_tier | base_price_per_1k ($) | Notes |
|----------------|------------------------|--------|
| Nano | 1 | |
| Micro | 10 | |
| Mid | 20 | |
| Macro | 50 | |
| Mega | 25 | If followers > 10,000,000 → **10** instead (“celebrity” rate) |

**Base price (before multipliers):**

\[
\text{base\_price} = \text{base\_price\_per\_1k} \times \frac{\text{followers}}{1000}
\]

### 2.4 Engagement rate multiplier

Uses `engagement_rate` (percentage, e.g. `3.0` for 3%):

| engagement_rate | engagement_multiplier |
|-----------------|----------------------|
| ≥ 5.0 | 1.5 |
| ≥ 3.0 | 1.2 |
| ≥ 1.0 | 1.0 |
| else | 0.7 |

### 2.5 Verification multiplier

| verified | verification_multiplier |
|----------|-------------------------|
| true | 1.3 |
| false | 1.0 |

### 2.6 Quality ratio multiplier

If `avg_likes > 0` **or** `avg_comments > 0`:

\[
\text{expected\_engagement} = \text{followers} \times \frac{\text{engagement\_rate}}{100}
\]

\[
\text{actual\_engagement} = \text{avg\_likes} + 3 \times \text{avg\_comments}
\]

(Comments are weighted **3×** vs likes.)

If `expected_engagement > 0`:

\[
\text{quality\_ratio} = \frac{\text{actual\_engagement}}{\text{expected\_engagement}}
\]

| quality_ratio | quality_multiplier |
|---------------|-------------------|
| ≥ 1.5 | 1.3 |
| ≥ 1.2 | 1.15 |
| ≥ 0.8 | 1.0 |
| ≥ 0.5 | 0.9 |
| else | 0.75 |

If neither likes nor comments are positive, `quality_multiplier` stays **1.0**.

### 2.7 Bot / low-signal multiplier

`bot_detection_multiplier` starts at **1.0**.

If `avg_likes > 0` and `avg_comments > 0`, let \(r = \text{avg\_comments} / \text{avg\_likes}\):

| r | bot_detection_multiplier (before second rule) |
|---|-----------------------------------------------|
| < 0.003 | 0.5 |
| < 0.005 | 0.7 |
| < 0.01 | 0.85 |
| else | 1.0 |

**Additional rule** if `followers > 10,000`:

\[
\text{min\_expected} = \text{followers} \times 0.005
\]

If `(avg_likes + avg_comments) < min_expected`, multiply `bot_detection_multiplier` by **0.6**.

### 2.8 Comment quality bonus

Starts at **1.0**. If `avg_likes > 0` and `avg_comments > 0`, with \(r = \text{avg\_comments} / \text{avg\_likes}\):

| r | comment_quality_bonus |
|---|------------------------|
| ≥ 0.05 | 1.25 |
| ≥ 0.03 | 1.15 |
| ≥ 0.01 | 1.0 |
| else | 1.0 |

### 2.9 Final estimate and band

\[
\text{base\_estimated\_price} = \text{base\_price} \times m_{\text{eng}} \times m_{\text{ver}} \times m_{\text{qual}} \times m_{\text{bot}} \times m_{\text{comment}}
\]

Where \(m_{\text{eng}}\) = engagement_multiplier, etc.

Point estimate:

\[
\text{estimated\_avg} = \text{base\_estimated\_price}
\]

**±30% band:**

\[
\text{estimated\_min} = 0.7 \times \text{estimated\_avg},\quad
\text{estimated\_max} = 1.3 \times \text{estimated\_avg}
\]

(Returned values are rounded to 2 decimals.)

The return dict’s `quality_multiplier` field is **not** the same as \(m_{\text{qual}}\) alone; it is:

\[
\text{quality\_multiplier (returned)} = \mathrm{round}\bigl(m_{\text{qual}} \times m_{\text{bot}} \times m_{\text{comment}},\; 2\bigr)
\]

---

## 3. Budget → tier probabilities (helper)

**Where:** `backend/core/utils/pricing.py` — `get_tier_from_budget(budget_min, budget_max)`

Used for interpreting budget ranges into tier-shaped distributions (e.g. prompts / auxiliary logic).

1. For each tier, define USD bands: Nano `[10,100)`, Micro `[100,1000)`, Mid `[1000,5000)`, Macro `[5000,25000)`, Mega `[25000, ∞)`.
2. Overlap length between `[budget_min, budget_max]` and each tier band is computed.
3. If total overlap &gt; 0, each tier’s probability = overlap length / total overlap (rounded to 2 decimals in code).
4. If there is **no** overlap, a discrete fallback is chosen from average budget thresholds (≤100 → Nano, ≤1000 → Micro, …).

---

## 4. Business–influencer matching (production API path)

**Where:** `backend/businesses/services/matching_service.py` — `find_best_matches()` and helpers.

The HTTP matcher uses ML predictions for the business, then scores influencers that pass filtering.

### 4.1 Stage 1 — Niche filter

- `business_top_niche` = class with **maximum** probability in `business_prediction['niche_probabilities']`.
- An influencer is kept only if **its** top niche (same argmax rule on `influencer.niche_probabilities`) **equals** `business_top_niche`.

Tier is **not** used as a hard filter in this path.

### 4.2 Cosine similarity (niche and legacy tier)

**Where:** `_cosine_similarity(probs1, probs2)`

1. `all_keys` = sorted union of keys from both dicts.
2. Build vectors \(v_1, v_2\) with missing keys as **0**.
3. Standard cosine similarity:

\[
\text{sim} = \frac{v_1 \cdot v_2}{\lVert v_1 \rVert \, \lVert v_2 \rVert}
\]

4. If either norm is 0 → **0**. Otherwise result is **clamped** to `[0, 1]`.

**Niche score:** cosine similarity between **business** and **influencer** niche probability vectors.

**Tier similarity score:** cosine similarity between **business** and **influencer** popularity probability vectors — computed for debugging/reference; **not** mixed into the main match score in `find_best_matches`.

### 4.3 Budget–price overlap score

**Where:** `_calculate_budget_price_overlap(budget_min, budget_max, price_min, price_max)`

Uses influencer **`estimated_price_min` / `estimated_price_max`** vs business budget.

\[
\text{overlap\_min} = \max(\text{budget\_min}, \text{price\_min}),\quad
\text{overlap\_max} = \min(\text{budget\_max}, \text{price\_max})
\]

\[
\text{overlap\_amount} = \max(0,\; \text{overlap\_max} - \text{overlap\_min})
\]

\[
\text{budget\_range} = \text{budget\_max} - \text{budget\_min}
\]

**Overlap score:**

\[
\text{budget\_match\_score} = \begin{cases}
\text{overlap\_amount} / \text{budget\_range} & \text{if } \text{budget\_range} > 0 \\[0.5em]
1.0 & \text{if } \text{budget\_range} = 0 \text{ and } \text{overlap\_amount} > 0 \\[0.5em]
0.0 & \text{if } \text{budget\_range} = 0 \text{ and no overlap}
\end{cases}
\]

**Affordability flag:**

\[
\text{is\_affordable} = (\text{price\_min} \le \text{budget\_max}) \land (\text{price\_max} \ge \text{budget\_min})
\]

**`price_position`:** `too_cheap` if `price_max < budget_min`; `too_expensive` if `price_min > budget_max`; else `within_budget`.

### 4.4 Audience location score (optional)

**Where:** `_audience_location_match_score(influencer, wanted_countries, wanted_cities)`

If **both** requested lists are empty after trimming → score **1.0** (neutral).

For each requested location string (lowercased), scan influencer JSON rows `audience_top_countries` / `audience_top_cities` (list of `{name, percentage}`):

- Match if normalized names are equal **or** one contains the other (substring).
- For each matched row, row score = `min(1, percentage / 100)`.
- Per requested token: best row score across rows.

**Countries:** average of per-country scores over all requested countries. **Cities:** same for cities.

**Final location score:** average of the **country** group average and **city** group average (only the groups that had at least one requested token). If somehow no parts → **1.0**.

### 4.5 Final match score (ranking)

Let \(N\) = niche cosine score, \(B\) = budget overlap score, \(L\) = location score.

**If the business sent at least one non-empty audience country or city** (after normalization):

\[
\text{total\_score} = 0.40\,N + 0.35\,B + 0.25\,L
\]

**Otherwise:**

\[
\text{total\_score} = 0.50\,N + 0.50\,B
\]

Matches are sorted by `total_score` **descending**; top `top_k` returned.

API responses expose `match_score` (= `total_score`), `niche_similarity` (= \(N\)), `budget_fit` (= \(B\)), `location_match_score` (= \(L\)).

---

## 5. Alternate matcher (library / legacy helper)

**Where:** `MatchingService.match_influencers_to_business()`

If used, **stage 1** still filters by top niche only. **Stage 2** score:

\[
\text{match\_score} = 0.6 \times \text{niche\_cosine} + 0.4 \times \text{popularity\_cosine}
\]

This path uses **popularity** cosine, **not** budget overlap. The live **`/api/businesses/match/`** flow uses **`find_best_matches`**, not this function—confirm in `businesses/views.py`.

---

## 6. ML service (probabilities, not hand-derived formulas)

**Where:** `ml_service/app/models/predictor.py`, `app/utils/parser.py`

- The service calls **Groq (LLaMA)** with structured prompts and expects **JSON-like** maps of niche and tier probabilities.
- The parser **normalizes** values so distributions behave like probability masses (e.g. renormalizing after parsing failures may yield uniform or adjusted weights—see code).
- There is **no closed-form equation** for niche outputs; behavior is model- and prompt-dependent.

Keep **`ml_service/app/constants.py`** label sets aligned with Django matching and pricing clamps.

---

## 7. Changelog discipline

When you change any formula above, update:

1. The implementation file  
2. **This document**  
3. User-facing copy if percentages or thresholds are shown in the UI  

---

## File index

| Topic | Primary file(s) |
|--------|-------------------|
| Engagement averages & rate | `backend/influencers/services/data_service.py` |
| Price estimate | `backend/core/utils/pricing.py` |
| Match score, cosine, overlap, location | `backend/businesses/services/matching_service.py` |
| Alternate 60/40 match | `matching_service.py` → `match_influencers_to_business` |
| ML outputs | `ml_service/app/models/predictor.py`, `app/utils/parser.py` |
