---
name: hotness_score
kind: function
lang: py
domain: datascience
version: "1.0.0"
purity: pure
signature: "def hotness_score(active_count: int, updated_at: datetime | None, now: datetime | None = None, half_life_days: float = 7.0) -> float"
description: "Calcula un score de hotness combinando frecuencia de acceso y recencia temporal. Util para ranking de resultados, memoria hot/cold y cache eviction."
tags: [ranking, decay, recency, frequency, scoring, cache, memory, datascience, pendiente-usar]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [math, datetime]
params:
  - name: active_count
    desc: "numero de accesos/activaciones de un item (ej: 150 views). Mayor count = score mas alto."
  - name: updated_at
    desc: "datetime de la ultima actualizacion. Si None, retorna siempre 0.0."
  - name: now
    desc: "datetime actual (defecto: ahora). Usar parametro explicitamente para tests deterministas."
  - name: half_life_days
    desc: "vida media para el decaimiento exponencial (defecto: 7.0 dias). Menor = decaimiento mas rapido."
output: "hotness score en rango [0, 1]. Combina frecuencia (active_count) y recencia (dias desde updated_at)."
tested: true
tests:
  - "active_count=0, updated_at reciente"
  - "active_count=100, updated_at reciente (score alto)"
  - "active_count=100, updated_at hace 30 dias (score bajo)"
  - "updated_at=None (retorna 0.0)"
  - "now explicito (determinista para tests)"
  - "half_life_days custom"
test_file_path: "python/functions/datascience/hotness_score_test.py"
file_path: "python/functions/datascience/hotness_score.py"
---

## Ejemplo

```python
from datetime import datetime, timedelta, timezone
from datascience.hotness_score import hotness_score

now = datetime.now(timezone.utc)

# Item reciente con muchos accesos -> score alto
score = hotness_score(active_count=150, updated_at=now - timedelta(hours=2), now=now)
# score > 0.95

# Item antiguo aunque muy accedido -> score bajo
score = hotness_score(active_count=150, updated_at=now - timedelta(days=30), now=now)
# score ~ 0.05

# Item sin fecha -> siempre 0
score = hotness_score(active_count=999, updated_at=None)
# score == 0.0
```

## Notas

Formula: `score = sigmoid(log1p(active_count)) * exp(-ln(2)/half_life_days * age_days)`

**Componente de frecuencia** — `sigmoid(log1p(count))` mapea enteros no negativos al rango `(0.5, 1.0)`:
- count=0 -> 0.5
- count=10 -> ~0.92
- count=100 -> ~0.99

**Componente de recencia** — decaimiento exponencial con vida media configurable:
- `half_life_days=7` (default): score se reduce a la mitad cada 7 dias
- `half_life_days=1`: decaimiento agresivo (util para feeds en tiempo real)
- `half_life_days=365`: decaimiento lento (util para contenido evergreen)

**Propiedades del score:**
- `updated_at=None` -> 0.0 siempre (item sin fecha no tiene hotness)
- `active_count=0, reciente` -> ~0.5 (neutro pero fresco)
- `active_count alto, reciente` -> ~1.0 (muy caliente)
- `active_count alto, antiguo` -> ~0.0 (frio a pesar de popularidad pasada)

Timestamps sin timezone se interpretan como UTC. Pasar `now` explicitamente garantiza determinismo en tests y reproducibilidad en pipelines batch.

Fuente conceptual: openviking/retrieve/memory_lifecycle.py (AGPL-3.0). Reimplementado desde cero con formula equivalente.
