From lifetimes#

If you have working lifetimes code, the same fit is shorter here, and the two knobs that quietly change the answer are named instead of hidden. lifetimes is archived and takes no new releases; clvkit fits the same four models on Python 3.11+.

Archived means nobody fixes it when the ground moves, and it already has. setuptools 83 removed pkg_resources, lifetimes.datasets stopped importing with it, and the load_cdnow_summary() line that opens every old tutorial now dies on a fresh install. The models themselves still fit; the on-ramp is what broke.

The whole migration, side by side#

lifetimes makes you build the RFM summary, then thread frequency, recency and T through every call by hand:

from lifetimes import BetaGeoFitter, GammaGammaFitter
from lifetimes.utils import summary_data_from_transaction_data

summary = summary_data_from_transaction_data(
    log, "customer_id", "date", monetary_value_col="amount", freq="W",
)

bgf = BetaGeoFitter(penalizer_coef=0.0)
bgf.fit(summary["frequency"], summary["recency"], summary["T"])

returning = summary[summary["frequency"] > 0]
ggf = GammaGammaFitter(penalizer_coef=0.0)
ggf.fit(returning["frequency"], returning["monetary_value"])

clv = ggf.customer_lifetime_value(
    bgf,
    summary["frequency"], summary["recency"], summary["T"],
    summary["monetary_value"],
    time=52, freq="W", discount_rate=0.001,
)

clvkit builds the summary once, as a CustomerBase, and every model reads it:

from clvkit import CustomerBase, CLV

cb = CustomerBase.from_transactions(log, time_unit="W", collapse="D")
clv = CLV().fit(cb).predict(horizon=52, discount_rate=0.001).to_pandas()

Call by call#

lifetimes

clvkit

summary_data_from_transaction_data(log, "customer_id", "date", monetary_value_col="amount", freq="W")

CustomerBase.from_transactions(log, time_unit="W", collapse="D")

BetaGeoFitter().fit(frequency, recency, T)

BGNBD().fit(cb)

ModifiedBetaGeoFitter().fit(frequency, recency, T)

MBGNBD().fit(cb)

GammaGammaFitter().fit(frequency, monetary_value)

GammaGamma().fit(cb)

bgf.conditional_probability_alive(frequency, recency, T)

bg.probability_alive()

bgf.conditional_expected_number_of_purchases_up_to_time(t, frequency, recency, T)

bg.predict(t)

ggf.conditional_expected_average_profit(frequency, monetary_value)

gg.predict()

ggf.customer_lifetime_value(bgf, frequency, recency, T, monetary_value, time=52, freq="W", discount_rate=0.001)

CLV().fit(cb).predict(horizon=52, discount_rate=0.001)

What changes#

One object, not three arrays. lifetimes hands you frequency, recency and T and trusts you to pass the right one to every call. Line them up wrong and the model still fits, on the wrong data, without a word. CustomerBase holds them together, so there’s nothing to line up.

The reporting unit and the event grain are separate. lifetimes folds both into one freq. clvkit splits them: time_unit is the clock the parameters are reported in, collapse is the grain at which same-period purchases count as one event. freq="W" in lifetimes silently deletes every second purchase inside a week; time_unit="W", collapse="D" keeps them and reports in weeks, the pair that reproduces the published CDNOW fit. Get it wrong and α moves 55%, from 4.41 to 6.85.

Swapping the transaction model is one line. BGNBD to MBGNBD is a one-word change on the same cb, with nothing re-summarised. In lifetimes you re-thread the arrays into a different fitter.

See CustomerBase, the shared contract for the contract these all read, or CDNOW: from a raw transaction log to lifetime value for the CDNOW fit end to end.