Skip to content

Metrics Reference

philanthropy.metrics

Donor KPI calculators.

donor_retention_rate(current_donors, prior_donors)

Share of the prior period's donors who gave again this period.

Returns a fraction in [0.0, 1.0]; 0.0 when prior_donors is empty (no base to retain from).

Source code in philanthropy/metrics/scoring.py
def donor_retention_rate(
    current_donors: Collection,
    prior_donors: Collection,
) -> float:
    """Share of the prior period's donors who gave again this period.

    Returns a fraction in ``[0.0, 1.0]``; ``0.0`` when ``prior_donors`` is
    empty (no base to retain from).
    """
    current_set = set(current_donors)
    prior_set = set(prior_donors)

    if not prior_set:
        return 0.0

    retained = current_set & prior_set
    return len(retained) / len(prior_set)

donor_acquisition_cost(total_fundraising_expense, new_donors_acquired)

Average spend to acquire one new donor.

Returns np.inf when new_donors_acquired is 0 (spend with nothing acquired), so the result is always safe to compare or plot.

Source code in philanthropy/metrics/scoring.py
def donor_acquisition_cost(
    total_fundraising_expense: float,
    new_donors_acquired: int,
) -> float:
    """Average spend to acquire one new donor.

    Returns ``np.inf`` when ``new_donors_acquired`` is 0 (spend with nothing
    acquired), so the result is always safe to compare or plot.
    """
    if new_donors_acquired == 0:
        return np.inf

    return total_fundraising_expense / new_donors_acquired

donor_lifetime_value(average_donation, lifespan_years, discount_rate=0.05, retention_rate=None)

Computes the Net Present Value (NPV) of a donor's future giving.

If a retention_rate is provided, the expected lifespan is dynamically computed as: expected_lifespan = 1 / (1 - retention_rate) Otherwise, the provided lifespan_years is used directly.

The mathematical formula for the NPV of an annuity is used:

If discount_rate > 0: NPV = average_donation * (1 - (1 + discount_rate) ** (-lifespan)) / discount_rate If discount_rate == 0: NPV = average_donation * lifespan

Parameters:

Name Type Description Default
average_donation float

The average annual donation amount.

required
lifespan_years float

The fixed number of years the donor is expected to continue giving. Only used if retention_rate is None.

required
discount_rate float

The discount rate used to compute the net present value of future gifts (e.g., 0.05 for 5%).

0.05
retention_rate float

The annual retention rate of the donor (e.g., 0.80 for 80%). If provided, this overrides lifespan_years by dynamically calculating expected lifespan.

None

Returns:

Type Description
float

The calculated Net Present Value of the expected donor lifetime value.

Source code in philanthropy/metrics/_financial.py
def donor_lifetime_value(
    average_donation: float, 
    lifespan_years: float, 
    discount_rate: float = 0.05, 
    retention_rate: float = None
) -> float:
    """
    Computes the Net Present Value (NPV) of a donor's future giving.

    If a retention_rate is provided, the expected lifespan is dynamically computed as:
        expected_lifespan = 1 / (1 - retention_rate)
    Otherwise, the provided lifespan_years is used directly.

    The mathematical formula for the NPV of an annuity is used:

    If discount_rate > 0:
        NPV = average_donation * (1 - (1 + discount_rate) ** (-lifespan)) / discount_rate
    If discount_rate == 0:
        NPV = average_donation * lifespan

    Parameters
    ----------
    average_donation : float
        The average annual donation amount.
    lifespan_years : float
        The fixed number of years the donor is expected to continue giving. 
        Only used if retention_rate is None.
    discount_rate : float, default=0.05
        The discount rate used to compute the net present value of future gifts 
        (e.g., 0.05 for 5%).
    retention_rate : float, default=None
        The annual retention rate of the donor (e.g., 0.80 for 80%). If provided, 
        this overrides lifespan_years by dynamically calculating expected lifespan.

    Returns
    -------
    float
        The calculated Net Present Value of the expected donor lifetime value.
    """
    if retention_rate is not None:
        if retention_rate >= 1.0:
            return float('inf')
        elif retention_rate < 0.0:
            raise ValueError("retention_rate cannot be negative.")

        lifespan = 1 / (1 - retention_rate)
    else:
        lifespan = lifespan_years

    if lifespan < 0:
        raise ValueError("lifespan_years cannot be negative.")

    if discount_rate == 0:
        return average_donation * lifespan
    elif discount_rate < 0:
        raise ValueError("discount_rate cannot be negative.")

    return average_donation * (1 - (1 + discount_rate) ** (-lifespan)) / discount_rate