> For the complete documentation index, see [llms.txt](https://docs.abtasty.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.abtasty.com/commerce/recommendations/create-a-recommentation-strategy/repurchase-recommendation-algorithm/how-the-repurchase-algorithm-decides.md).

# How the repurchase algorithm decides

This article explains **why** the algorithm recommends the products it does. For the practical side (the variable, builtin, preset, input/output contract, and errors), see Integrate & call the repurchase algorithm.

### The core intuition

For each product in a user's purchase history, the algorithm:

1. Estimates **D**, the product's repurchase cadence in days.
2. Computes `days_since_last_purchase`, how long since the user last bought it.
3. Defines a **repurchase window** around D.
4. If the user is inside the window, the product is eligible and gets an urgency score; otherwise it's discarded.

Eligible products are returned sorted by descending urgency. The idea: don't recommend a product right after it was bought (too early), nor indefinitely long after (the need is probably already covered), but around the moment the observed cadence says the user is due to buy again.

### Estimating the cadence D

How D is estimated depends on the user's history **for that specific product**.

#### Two or more purchases: use the user's own cadence

Sort the purchase dates, take the intervals between consecutive purchases, and set D to their mean.

> **Example.** Purchases on Jan 1, Mar 1, May 1 → intervals of 59 and 61 days → **D = 60 days.**

The user's own cadence is used as-is, with no smoothing and no blending with the site-wide average.

#### A single purchase: fall back to the site-wide average

One purchase can't measure an interval, so the algorithm falls back to the product's site-wide average cadence (`avg_repurchase_interval_days`).

* If a site-wide average exists → it becomes D.
* If none exists (nobody has ever repurchased the product) → the product is **excluded**. There is no second fallback.

{% hint style="info" %}
The `repurchase_frequency` column always reflects the value **actually used**: the user's own cadence, or the site-wide average when it's the fallback.
{% endhint %}

### The site-wide average

The site-wide average is the safety net for users who bought a product only once. For each product:

1. Consider purchases over the **trailing 12 months**.
2. Keep only users who bought that product **at least twice** on distinct dates. A one-time buyer says nothing about cadence.
3. For each such user, compute their average cadence (mean of the intervals between their successive purchases).
4. The product's site cadence is the **mean of those per-user cadences**, where every multi-buyer weighs equally regardless of how many times they bought.

Two product-level indicators come out of this:

| Indicator                      | Meaning                                                                                                                    |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `avg_repurchase_interval_days` | Average repurchase cadence across all multi-buyers. Null if no multi-buyer exists → the product can't serve as a fallback. |
| `multi_buyer_count`            | Number of users who bought the product ≥ 2 times (a confidence indicator).                                                 |

These refresh with the catalog; a daily cadence is enough, since the 12-month aggregate barely moves day to day.

### The repurchase window

Around the estimated cadence D, an eligibility window is defined as:

```
[ D − ratio × D ,  D + ratio × D ]
```

With the default `ratio` of **1/3**, the window is `[2/3·D, 4/3·D]`.

{% hint style="warning" %}
The original brief said "±30%", but the implemented default is one third (1/3 ≈ 33.3%). **1/3 is the authoritative value.**
{% endhint %}

A product is eligible only when the user sits inside the window:

```
days_since_last = today − date of last purchase
eligible  ⇔  D × (1 − ratio)  ≤  days_since_last  ≤  D × (1 + ratio)
```

Before the window (too early) → excluded. After it (overdue) → excluded. The `ratio` is configured once per site (`repurchase_window_ratio` in Redis) and is shared by every repurchase rule on that site, so it's **not** a per-rule parameter. Per-rule tuning happens through `where` / `sort` on `days_since_last_purchase` and `urgency_score`.

> **Example.** D = 30 days → window `[20, 40]` days after the last purchase.

### The urgency score

The urgency score is a symmetric triangular peak centered on D: `1.0` exactly at the forecasted date, falling linearly to `0` at the window edges.

```
score
1.0 |          *
    |         / \
    |        /   \
    |       /     \
0.0 |______/       \______
       low      D     high
   (D·(1−ratio))   (D·(1+ratio))
```

The compact form:

```
urgency_score = max( 0 ,  1 − |days_since_last − D| / (D × ratio) )
```

With D = 30 and ratio = 1/3 (window `[20, 40]`):

| Days since last purchase | Urgency score |
| ------------------------ | ------------- |
| 20                       | 0.0           |
| 25                       | 0.5           |
| 30                       | 1.0           |
| 35                       | 0.5           |
| 40                       | 0.0           |

Sorting `urgency_score DESC` therefore surfaces the products for which the user is closest to their repurchase date.

### End-to-end example

Request excerpt:

```json
{
  "variables": {
    "purchase_history": {
      "COFFEE-1KG": ["2026-01-05T08:00:00", "2026-02-04T08:00:00", "2026-03-06T08:00:00"],
      "FILTERS-100": ["2026-02-20T08:00:00"]
    }
  }
}
```

Walkthrough as of **2026-04-02**, default ratio 1/3:

* **COFFEE-1KG**: 3 purchases → intervals of 30 and 31 days → **D ≈ 30.5**. Last purchase 03/06 → `days_since_last ≈ 27`. Window ≈ `[20.3, 40.7]`. 27 is inside → eligible, `urgency_score ≈ 0.65`.
* **FILTERS-100**: 1 purchase → fall back to the site-wide average. If `avg_repurchase_interval_days ≈ 45` → **D = 45**. Last purchase 02/20 → `days_since_last ≈ 41`. Window ≈ `[30, 60]`. 41 is inside → eligible. Had there been no site-wide average, it would be excluded.

The response returns the eligible products with their catalog columns plus `repurchase_frequency`, `days_since_last_purchase`, and `urgency_score`, sorted by `urgency_score DESC` and limited to 12.

{% hint style="info" %}
Reminder: the API doesn't fetch history on its own. The caller supplies `purchase_history` on every request. See Integrate & call the repurchase algorithm for how to wire that up.
{% endhint %}

### Defaults at a glance

| Parameter                  | Default                                 | Configured where                                   |
| -------------------------- | --------------------------------------- | -------------------------------------------------- |
| Input variable             | `purchase_history` (`ITEMS_HISTORY`)    | Declared automatically on analytics unlock         |
| Cap on products in history | 50 (most recent kept)                   | Not configurable                                   |
| Window width `ratio`       | 1/3 (≈ ±33%) → `[2/3·D, 4/3·D]`         | Per site (`repurchase_window_ratio`), not per rule |
| Site-wide average lookback | Trailing 12 months                      | Not configurable                                   |
| Multi-buyer filter         | Users with ≥ 2 purchases of the product | Not configurable                                   |
| Default sort               | `urgency_score DESC`                    | Editable in the rule                               |
| Default volume             | 12                                      | Editable in the rule                               |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.abtasty.com/commerce/recommendations/create-a-recommentation-strategy/repurchase-recommendation-algorithm/how-the-repurchase-algorithm-decides.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
