Sales Forecast: Predicting Unit Sales Using Finite Mixture Model with Beta-Geometric Distribution
Author
Abdullah Mahmood
Published
April 10, 2025
1 Introduction
The model discussed here is applicable for the following two-dimensional classification of customer base:
Opportunities for transactions: Continuous-Time
By “continuous-time” we mean that transactions can occur at any point in time.
Type of relationship with customers: Noncontractual
In a “noncontractual” setting, the absence of a contract or subscription means that the point in time at which the customer becomes inactive is not observed by the firm (e.g., a catalog retailer). The challenge is how to differentiate between a customer who has ended a “relationship” with the firm versus one who is merely in the midst of a long hiatus between transactions.
The objective here is to incorporate an easily implementable model of buyer behavior capable of generating a medium-term forecast of aggregate purchasing by a cohort of customers. Aggregate-level forecasts are a critical input to any attempt to value a customer base, and serve as a diagnostic to help gauge the effectiveness of various short-term marketing programs (e.g., the provision of a baseline sales estimate against which the performance of a promotion can be evaluated).
First, we discuss the background to the modeling exercise and present the dataset on which this work is based. We then develop the stochastic model of buyer behavior which is used to generate forecasts of future CD purchasing. This is followed by the empirical analysis where we examine the fit of the proposed model and its forecasting ability.
Note to user interested in using this model:
The data being modeled are counts of relatively homogeneous units (e.g., CDs). This model must not be used to model dollar sales or counts of products that are not very similar (e.g., the number of products purchased at Amazon.com where the units include such disparate items as books, electronic equipment, lawn furniture, and so on).
Use of the shifted-geometric/geometric distributions implies that the modal trial quantity is 1 unit and that the modal number of units purchased in subsequent weeks, conditional on being a “possible repeat buyer”, is 0. If this is not that case, it will be necessary to change the underlying model structure. For example, the shifted beta-geometric model of trial counts could be replaced by the truncated or shifted NBD (which can have a mode away from 1). Similarly, the beta-geometric repeat purchasing distribution could be replaced by the NBD (which can have a non-zero mode).
A number of other assumptions made, implicitly or explicitly, in the paper should also be acknowledged and taken into account (e.g., the independence of quantity decisions across transactions).
2 Imports
2.1 Import Packages
Code
import polars as plimport numpy as npimport altair as altfrom utils import ChartTemp, layered_line_propfrom scipy.optimize import minimizefrom scipy.stats import chi2, betafrom scipy.linalg import toeplitzfrom great_tables import GT from IPython.display import display_markdownalt.renderers.enable("html")
RendererRegistry.enable('html')
2.2 Import Transaction Data
For the purposes of this work, we focus on a single cohort of new customers who made their first purchase at the CDNOW website in the first quarter of 1997. We have data covering their initial (“trial”) and subsequent (“repeat”) purchases for the three month period (1/97 - 3/97) during which over 23,000 individuals purchased nearly 70,000 units (CDs). We are interested in forecasting the future (repeat) purchasing of these customers using a model calibrated with these quarter 1 data.
Code
CDNOW_master = ( pl.scan_csv(source ='data/CDNOW/CDNOW_master.csv', has_header=False, separator=',', schema={'CustID': pl.Int32, # customer id'Date': pl.String, # transaction date'Quant': pl.Int16, # number of CDs purchased'Spend': pl.Float64}) # dollar value (excl. S&H) .with_columns(pl.col('Date').str.to_date("%Y%m%d")) .with_columns((pl.col('Date') - pl.date(1996,12,31)).dt.total_days().cast(pl.UInt16).alias('PurchDay')) .with_columns((pl.col('Spend')*100).round(0).cast(pl.Int64).alias('Spend Scaled')) .group_by('CustID', 'Date') .agg(pl.col('*').exclude('PurchDay').sum(), pl.col('PurchDay').max()) # Multiple transactions by a customer on a single day are aggregated into one .sort('CustID', 'Date') .with_columns((pl.col("CustID").cum_count().over("CustID") -1).cast(pl.UInt16).alias("DoR")) # DoR = Depth of Repeat ('Transaction' time: starts with 0 as trial, 1 as 1st repeat and so on))display(CDNOW_master.head().collect())
shape: (5, 7)
CustID
Date
Quant
Spend
Spend Scaled
PurchDay
DoR
i32
date
i64
f64
i64
u16
u16
1
1997-01-01
1
11.77
1177
1
0
2
1997-01-12
6
89.0
8900
12
0
3
1997-01-02
2
20.76
2076
2
0
3
1997-03-30
2
20.76
2076
89
1
3
1997-04-02
2
19.54
1954
92
2
2.3 Create Number of Units Purchased by Week of Purchase Summary
For the cohort of customers who first purchased at the CDNOW website in the first quarter of 1997, we choose to work with the summary of total purchasing as presented in the last table below (TableOne). This gives the distribution of the number of units purchased for each of the twelve weeks, along with details of total purchasing (weeklysales) and the number of new customers (triers) in each week (weeklytriers).
Code
# What is the total number of CDs purchased each week?weeklysales = ( CDNOW_master .with_columns(((pl.col('PurchDay') -1) //7+1).alias('Week')) .group_by('Week') .agg(pl.col('Quant').sum()) .sort('Week'))( ChartTemp(weeklysales.collect()) .line_encode(y_col='Quant', y_title='Units Purchased', x_range=78) .line_prop('Total Weekly Sales'))
Code
# How many people made their first-ever (“trial”) purchase each week?weeklytriers = ( CDNOW_master .filter(pl.col('DoR') ==0) .with_columns(((pl.col('PurchDay') -1) //7+1).alias('Week')) .group_by('Week') .agg(pl.len().alias('Incremental Triers')) .sort('Week'))
Code
# What is the total number of CDs purchased by triers in their trial week?weeklytrierquant = ( CDNOW_master .with_columns(((pl.col('PurchDay') -1) //7+1).alias('Week')) .with_columns(pl.when(pl.col('DoR') ==0).then(pl.col('Week')).fill_null(strategy='forward').over('CustID').alias('Trial Week')) .filter(pl.col('Trial Week') == pl.col('Week')) # Any repeat purchasing by a customer in their trial week is added to their trial purchase .group_by('Week') .agg(pl.col('Quant').sum().alias('Triers Quant')) .sort('Week'))
Code
# What is the weekly total sales split between "trial" and "repeat"?weeklysalessplit = ( weeklysales .join(weeklytrierquant, on='Week', how='left') .fill_null(0) .with_columns( (pl.col('Quant') - pl.col('Triers Quant')).alias('Repeat Sales') ) .rename({'Quant': 'Total Sales', 'Triers Quant': 'Trial Sales'}))# Weekly total sales split for the 12 weeks (corresponding to trial purchases) - Long-Form Dataweeklysalessplit_lf = ( weeklysalessplit .filter(pl.col('Week') <=12) .unpivot(index='Week', on=['Total Sales', 'Trial Sales', 'Repeat Sales'], variable_name='Actual Sales', value_name='Sales'))# Weekly repeat salesweekly_repeat_df = ( weeklysalessplit .select('Week', 'Repeat Sales'))
Code
dist_table = ( CDNOW_master .with_columns(((pl.col('PurchDay') -1) //7+1).alias('Week')) .filter(pl.col('Week') <=12) .group_by('CustID', 'Week') .agg(pl.col('Quant').cast(pl.Int32).sum()) # Sum quantity purchased by each customer within each week .group_by('Week', 'Quant') .agg(pl.col('CustID').len().alias('Count')) .sort('Quant') .collect())dist_table_10_plus = ( dist_table.filter(pl.col('Quant') >=10) .group_by('Week') .agg(pl.col('Count').sum()) .sort('Week') .with_columns(pl.lit(10).alias('Quant')) .select('Week', 'Quant', 'Count'))dist_table_1 = dist_table.filter(pl.col('Quant') <10).vstack(dist_table_10_plus)cumweeklytriers = ( weeklytriers.collect() .with_columns(pl.col('Incremental Triers').cum_sum()) .join(dist_table_1.group_by('Week').agg(pl.col('Count').sum()), on='Week', how='left') .with_columns((pl.col('Incremental Triers') - pl.col('Count')).alias('Count')) .with_columns(pl.lit(0).alias('Quant')).select('Week', 'Quant', 'Count'))TableOne = ( dist_table_1.vstack(cumweeklytriers).sort('Week', 'Quant') .pivot(on='Week', index='Quant', values='Count'))( GT(TableOne, rowname_col='Quant') .tab_header(title="Distribution of the Number of CDNOW Units Purchased", subtitle='First 12 Weeks') .tab_stubhead('Units Purchased') .fmt_integer() .tab_spanner(label='Week', columns=[str(i) for i inrange(1, 13)]) .opt_stylize())
Distribution of the Number of CDNOW Units Purchased
First 12 Weeks
Units Purchased
Week
1
2
3
4
5
6
7
8
9
10
11
12
0
0
1,478
3,033
4,763
6,608
8,616
10,829
12,716
14,698
16,774
18,881
20,902
1
750
852
984
1,066
1,237
1,262
1,204
1,278
1,397
1,444
1,387
1,148
2
383
387
456
484
566
649
592
606
644
659
677
663
3
191
214
270
267
293
320
302
343
365
374
355
367
4
95
120
114
161
163
196
156
195
179
187
199
182
5
55
72
68
89
96
96
80
100
95
118
94
120
6
36
40
42
40
51
54
65
45
75
71
72
54
7
18
12
27
30
36
40
39
31
41
37
30
43
8
12
15
9
21
19
21
20
24
23
29
24
32
9
9
9
8
9
21
14
21
8
14
9
12
16
10
25
17
27
32
36
55
39
35
48
42
50
43
While this is a very convenient summary of the customers’ purchasing, it suffers from two critical shortcomings: (1) we have no explicit information on the breakdown of trial vs. repeat sales in each week, and (2) we cannot see the longitudinal series of purchase events at the household level, thereby making it impossible to construct a standard model of repeat purchasing (i.e., depth of repeat or counting). We must therefore develop a model of week-by-week repeat purchasing whose parameters can be estimated using the above data.
3 Model Development
3.1 Introduction
Our objective to is develop a simple stochastic model of buyer behavior capable of producing a medium-term forecast of unit purchases by the cohort of new customers whose total purchasing during the first quarter of 1997 is summarized in TableOne.
Let us consider these data more carefully, focusing first on the column corresponding to week 2. This reports the distribution of purchase quantity for that week by the 3216 customers who could have made a purchase. This set is comprised of 1642 customers who made their first purchases at the CDNOW website in week 2, as well as 1574 customers who first purchased at it in week 1 and who therefore may be back in the market for additional (repeat) purchases in week 2. By definition, the 1642 week 2 triers must have purchased at least one unit. This implies that the 1478 people who made no purchase at the website in week 2 must be customers who made a trial purchase in week 1. Thus we have 1574 − 1478 = 96 week 1 triers who made repeat purchases in week 2. In other words, this observed distribution of week 2 purchasing represents a mixture of purchases by those whose first purchase occasion occurred in week 2 and repeat purchases by those who tried in week 1.
Therefore, the probability of observing someone purchasing \(x\) units in week 2 is simply a weighted average of the probability that a week 2 trier bought \(x\) units during her initial week, and the probability that a week 1 trier bought \(x\) units on at least one repeat purchase occasion in week 2. The weights are determined by the number of triers in weeks 1 and 2, i.e.,
where \(P(T_2 = x)\) is the probability that a randomly chosen customer making her first purchase(s) at CDNOW in week 2 buys \(x\) units, and \(P(R_{2 \mid 1} = x)\) is the probability that a randomly chosen customer who first purchased in week 1 purchases \(x\) units in week 2.
Similarly, the column corresponding to week 3 reports the distribution of purchase quantity for the 5038 customers who could have made a purchase that week: 3216 of these customers made their first purchase in weeks 1 or 2, and 3033 of these people made no (repeat) purchase in week 3. We therefore have 183 week 1 and week 2 trialists making repeat purchases in this week, but we do not observe the specific number of week 1 versus week 2 triers, nor each of these groups’ respective distribution of units purchased. Extending the same logic from above, however, we can express the probability of observing \(x\) purchases in week 3 as a weighted average of the probability that a week 3 trier made \(x\) purchases, the probability that a week 2 trier made \(x\) repeat purchases in week 3, and the probability that a week 1 trier made \(x\) repeat purchases in week 3, i.e.,
where the weights are determined by the number of triers in weeks 1-3.
Code
# Week 2 print('Week 2')# Number of potential buyers in week 2:print('- # of potential buyers in week 2:', TableOne['2'].sum())# Number of triers:w2treirs = weeklytriers.filter(pl.col('Week') ==2).select('Incremental Triers').collect().item(0,0)print('- # of triers:', w2treirs)# Week 1 triers:print('- # of week 1 triers in week 2:', TableOne['2'].sum() - w2treirs)# Week 1 triers who made no purchase in week 2:print('- # of week 1 triers who made no repeat purchase in week 2:', TableOne['2'][0])# Week 1 triers who made a purchase in week 2:print('- # of week 1 triers who made a purchase in week 2:', TableOne['2'].sum() - w2treirs - TableOne['2'][0])# Week 3print('Week 3')# Number of potential buyers in week 3:print('- # of potential buyers in week 3:', TableOne['3'].sum())# Number of triers:w3treirs = weeklytriers.filter(pl.col('Week') ==3).select('Incremental Triers').collect().item(0,0)print('- # of triers:', w3treirs)# Week 1 & 2 triers:print('- # of week 1 & 2 triers in week 3:', TableOne['3'].sum() - w3treirs)# Week 1 & 2 triers who made no purchase in week 2:print('- # of week 1 & 2 triers who made no repeat purchase in week 3:', TableOne['3'][0])# Week 1 & 2 triers who made a purchase in week 3:print('- # of week 1 triers who made a purchase in week 2:', TableOne['3'].sum() - w3treirs - TableOne['3'][0])
Week 2
- # of potential buyers in week 2: 3216
- # of triers: 1642
- # of week 1 triers in week 2: 1574
- # of week 1 triers who made no repeat purchase in week 2: 1478
- # of week 1 triers who made a purchase in week 2: 96
Week 3
- # of potential buyers in week 3: 5038
- # of triers: 1822
- # of week 1 & 2 triers in week 3: 3216
- # of week 1 & 2 triers who made no repeat purchase in week 3: 3033
- # of week 1 triers who made a purchase in week 2: 183
More generally, the distribution of purchases in week \(w\) can be modeled using a finite mixture model with known mixing weights:
where \(n_{i}\) is the number of triers in week \(i\) (i.e., customers making their first purchase(s) at the CDNOW website), \(P(T_w = x)\) is the probability that a randomly chosen customer making her first purchase(s) at CDNOW in week \(w\) buys \(x\) units, and \(P(R_{w \mid i} = x)\) is the probability that a randomly chosen customer who first purchased in week \(i\) buys \(x\) units in week \(w\). We therefore need to develop submodels for \(P(T_w = x)\) and \(P(R_{w \mid i} = x)\).
3.2 Modeling Trial Purchases
Let the random variable \(T_w\) denote the number of units purchased in week \(w\) by a customer whose trial purchase occurs in week \(w\). (Note that, by definition, \(T_w\) is a zero-truncated discrete random variable.) Our submodel for the distribution of \(T_w\) is based on the following two assumptions:
At the level of the individual customer, \(T_w\) is distributed according to a shifted geometric distribution with parameter \(q_T\) and probability mass function
\(q_T\) is distributed across the population according to a beta distribution with parameters \(\alpha_T\) and \(\beta_T\) , and probability density function
The intuition associated with these two assumptions is as follows. The geometric distribution corresponds to purchasing following a “coin-flipping” process in which the individual customer keeps buying until she tosses a “head”. The beta distribution is simply a means of allowing P(“heads”) to vary across the customer base.
It follows that the aggregate distribution of the number of units purchased by a week \(w\) trialist is given by
which we call the shifted beta-geometric distribution. Elsewhere in the marketing literature, this distribution was used by Morrison and Perry (1970) as a quantity submodel in their NBD-based model of purchase frequency and purchase quantity. The mean of this distribution is given by
Let the random variable \(R_{w \mid i}\) denote the number of (repeat) purchases made in week \(w\) by a customer who made her trial purchase in week \(i (w > i)\). Specifying an appropriate model for the distribution of \(R_{w \mid i}\) is the single most important step in this modeling effort. To do so, we will start with the assertion that the purchasing by a new customer at an established store (or website) is analogous to a consumer’s purchasing of a new product. We know that repeat buying rates for new products tend to be nonstationary - at least early in a new product’s life - with the purchase rate declining (towards an equilibrium level) over time. One way to capture this pattern is to assume that, for a given cohort, the number of people making zero purchases in a given week grows (at a decreasing rate), which means that the observed average number of units purchased decreases over time.
Our submodel for the distribution of \(R_{w \mid i}\) is based on the following three assumptions:
Assumption 1:
In week \(w\), existing customers are either out of the market, i.e., definitely not going to make a repeat purchase that week, or a possible repeat buyer. (The notion that someone is a “possible repeat buyer” does not ensure that she will actually purchase any units that week; it merely conveys the fact that she will consider purchasing with some non-zero probability.) The probability of a week \(i\) trialist being out of the market in week \(w\), which we denote by \(\pi_{w \mid i}\), is assumed to be governed by the following time-dependent distribution:
\[
\pi_{w|i} = 1 - \gamma (w - i)^\delta, \quad w > i
\]
When \(\alpha < 0, \pi_{w \mid i}\) grows at a decreasing rate as \(w − i\) increases; consequently, the number of week \(i\) triers making zero purchases in week \(w\) increases over time. Likewise, \(\delta\) can also be positive, allowing for the possibility that the number of repeat buyers actually increases over time. (Note that while this fraction \(\pi_{w \mid i}\) of buyers is definitely not going to make a repeat purchase in week \(w\), we are not assuming that they are permanently out of the market, i.e., they may consider buying again in future weeks.)
Assumption 2:
For an individual who has been classified as a “possible repeat buyer” in week \(w\), the number of units purchased, \(R_{w}\), is distributed according to a geometric distribution with parameter \(q_{R}\) and probability mass function
Qualitatively, the same type of “coin-flipping” story as discussed earlier for the trial submodel applies here as well. Note, however, that there are two differences. First, there is no longer a truncation at zero, i.e., the first coin-flip determines whether a “possible repeat buyer” actually chooses to purchase one unit (or more). Second, the stopping probability (P(“heads”)) is governed by a different beta distribution than that used for the trial purchasing process.
It follows that the aggregate distribution of the number of units purchased in week \(w\) by a week \(i\) trialist \((w > i)\) is given by: \[
\begin{align*}
P(R_{w \mid i} = x) &= \delta_{x=0} \pi_{w \mid i} + (1 - \pi_{w \mid i}) \int_0^1 P(R_w = x \mid q_R) g(q_R) \, dq_R \\
&= \delta_{x=0} \pi_{w \mid i} + (1 - \pi_{w \mid i}) \frac{B(\alpha_R + 1, \beta_R + x)}{B(\alpha_R, \beta_R)},
\end{align*}
\]
where \(\delta_{x=0}\), the Kronecker delta, equals \(1\) if \(x = 0\), and \(0\) otherwise. We call this the “time-dependent, zero-inflated beta-geometric” distribution. The mean of this distribution is
As a refresher (or primer) on estimating the parameters of a basic probability model using SciPy, let us consider fitting the trial submodel to the week 1 data. The column of data in TableOne corresponding to week 1 presents trial-week-only purchases by a group of 1574 customers. Our goal is to fit the following shifted beta-geometric model to these data:
The probability of making 10+ purchases in a trial week is simply:
\[
1-\sum^{9}_{x=1}P(T_{1}=x)
\]
The shifted beta-geometric model has two parameters, \(\alpha_{T}\) and \(\beta_{T}\) . Maximum likelihood estimates of these two model parameters are found by maximizing the following log-likelihood function:
As the value of the sample test statistic is less than the critical value, we conclude that the shifted beta-geometric distribution adequately fits the data.
Code
w1_trial_df = pl.DataFrame({'Number of Units Purchased':np.arange(1,11), 'Observed': n1x[1:], 'Expected': E_n1x[1:]})w1_trial_df = w1_trial_df.unpivot(on=['Observed', 'Expected'], index='Number of Units Purchased', variable_name='Legend' , value_name='Frequency')ChartTemp(w1_trial_df).mark_bar().encode( x=alt.X('Number of Units Purchased:O', axis=alt.Axis(labelAngle=0)), y='Frequency:Q', color='Legend:N', xOffset='Legend:N').line_prop('Fit of Week 1 Trial Model')
Calibrating the Full Model
We now turn our attention to the task of estimating the parameters of the full model. Our goal is to construct the following log-likelihood function:
where \(n_{wx}\) is the number of people making \(x\) purchases in week \(w\).
At the heart of this log-likelihood function is \(P(X_{w} = x)\), the probability that an eligible customer purchases \(x\) units in week \(w\), as given in the equation:
where \(\delta_{x=0}\), the Kronecker delta, equals \(1\) if \(x = 0\), and \(0\) otherwise. We call this the “time-dependent, zero-inflated beta-geometric” distribution.
Probabilities associated with the “time dependent, zero-inflated beta-geometric” distribution can be computed using the following forward recursive relationship:
However, rather than directly use the recursive relationship given in the equation above, we take the following apporach. We first compute the probability that a week 1 trier is a “possible repeat buyer” in weeks 2-12 using \(\gamma(w-1)^{\delta}\).
Next we need to create the expressions for the beta-geometric probabilities of making \(x\) purchases \((x = 0, 1, \ldots , 9, 10+)\), given \(\alpha_R\) and \(\beta_R\), for someone who is a “possible repeat buyer”. The beta-geometric probabilities can be computed using the following recursive relationship:
\[
P(R = x \mid \text{possible repeat buyer}) =
\begin{cases}
\frac{\alpha_R}{\alpha_R + \beta_R}, & x = 0 \\[10pt]
\frac{\beta_R + x - 1}{\alpha_R + \beta_R + x} P(R = x - 1), & x \ge 1 \\[10pt]
\end{cases}
\]
The probability of a “possible repeat buyer” making 10+ purchases is simply \(1 - \sum^{9}_{x=0} P(R=x)\).
As the sample data are in the form of a table documenting the number of people purchasing \(0, 1, \ldots , 9, 10+\) units (CDs) for each of the 12 weeks, we need to create a table that gives us \(P(X_w = x), x = 0, 1, \ldots , 9, 10+\) and \(w = 1, 2, \ldots 12\), given values of the six model parameters \((\alpha_T, \beta_T, \alpha_R, \beta_R, \gamma, \delta)\).
We see that \(P(X_w = x)\) is simply a weighted average of the week-of-trial-specific probabilities of purchasing \(x\) units in week \(w\). As an intermediate step, we build twelve tables that give us the probability of purchasing \(x\) units in week \(w\), one for each trial week. These will then be aggregated and the log-likelihood created.
e_distTable = ( pl.DataFrame({'Quant': np.arange(11, dtype=np.int32)}) .hstack(pl.from_numpy(e_distMAT.astype(np.int32), schema=TableOne.columns[1:])))( GT(e_distTable, rowname_col='Quant') .tab_header(title="Estimated Distribution of the Number of CDNOW Units Purchased", subtitle='First 12 Weeks') .tab_stubhead('Units Purchased') .fmt_integer() .tab_spanner(label='Week', columns=[str(i) for i inrange(1, 13)]) .opt_stylize())
Estimated Distribution of the Number of CDNOW Units Purchased
First 12 Weeks
Units Purchased
Week
1
2
3
4
5
6
7
8
9
10
11
12
0
0
1,473
3,028
4,762
6,599
8,668
10,776
12,727
14,689
16,807
18,898
20,872
1
771
847
973
1,061
1,216
1,273
1,227
1,264
1,375
1,394
1,363
1,269
2
367
405
467
511
586
615
595
614
669
679
666
622
3
186
207
239
263
302
318
309
319
348
354
348
326
4
100
112
130
143
164
174
169
175
191
195
192
181
5
56
63
74
81
94
99
97
101
110
112
111
105
6
33
37
43
48
56
59
58
60
66
67
67
64
7
20
22
26
29
34
36
36
37
41
42
42
40
8
12
14
16
18
21
23
23
24
26
27
27
26
9
8
9
11
12
14
15
15
15
17
18
18
17
10
17
21
25
28
33
36
37
39
42
44
45
43
Code
display_markdown(f'''$\\chi^{{2}}$ = {chi_square:0.4f}Degrees of freedom = {df}Critical Value = {critical_value:0.4f}''', raw=True)
\(\chi^{2}\) = 129.2159
Degrees of freedom = 113
Critical Value = 138.8114
As the value of the sample test statistic is less than the critical value, we conclude that the model adequately fits the data.
full_weeks = np.arange(52) +1# expected number of units purchased in weeks 1–52 by any given week 1 trier week_1_triers = np.zeros(full_weeks.shape[0])week_1_triers[0] = (alpha_T + beta_T -1)/(alpha_T -1) # expected number of units purchased in the trial week week_1_triers[1:] = gamma * (full_weeks[1:] -1)**delta * beta_R / (alpha_R -1) # expected repeat sales, number of units purchased w−i weeks after trial# mean weekly unit purchases for any week i trier# expected total number of units sold in any given week ismean_weekly_unit_purchases = toeplitz(week_1_triers, r=np.zeros(trial_weeks.shape[0]))# estimate of weekly unit sales - expected total number of units sold in weeks 1–52e_weekly_sales = np.sum(incr_triers * mean_weekly_unit_purchases, axis=1)