#   Copyright 2022 - 2025 The PyMC Labs Developers
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
r"""Bass diffusion model for product adoption.
Adapted from Wiki: https://en.wikipedia.org/wiki/Bass_diffusion_model
The Bass diffusion model, developed by Frank Bass in 1969, is a mathematical model that describes
the process of how new products get adopted in a population over time. It is widely used in
marketing, forecasting, and innovation studies to predict the adoption rates of new products
and technologies.
Mathematical Formulation
-----------------------
The model is based on a differential equation that describes the rate of adoption:
.. math::
    \frac{f(t)}{1-F(t)} = p + q F(t)
Where:
- :math:`F(t)` is the installed base fraction (cumulative proportion of adopters)
- :math:`f(t)` is the rate of change of the installed base fraction (:math:`f(t) = F'(t)`)
- :math:`p` is the coefficient of innovation or external influence
- :math:`q` is the coefficient of imitation or internal influence
The solution to this equation gives the adoption curve:
.. math::
    F(t) = \frac{1 - e^{-(p+q)t}}{1 + (\frac{q}{p})e^{-(p+q)t}}
The adoption rate at time t is given by:
.. math::
    f(t) = (p + q F(t))(1 - F(t))
Key Parameters
-------------
The model has three main parameters:
- :math:`m`: Market potential (total number of eventual adopters)
- :math:`p`: Coefficient of innovation (external influence) - typically 0.01-0.03
- :math:`q`: Coefficient of imitation (internal influence) - typically 0.3-0.5
Parameter Interpretation
-----------------------
- A higher :math:`p` value indicates stronger external influence (advertising, marketing)
- A higher :math:`q` value indicates stronger internal influence (word-of-mouth, social interactions)
- The ratio :math:`q/p` indicates the relative strength of internal vs. external influences
- The peak of adoption occurs at time :math:`t^* = \frac{\ln(q/p)}{p+q}`
Applications
-----------
The Bass model has been applied to forecast the adoption of various products and technologies:
- Consumer durables (TVs, refrigerators)
- Technology products (smartphones, software)
- Pharmaceutical products
- Entertainment products
- Services and subscriptions
This implementation provides a Bayesian version of the Bass model using PyMC, allowing for:
- Uncertainty quantification through prior distributions
- Hierarchical modeling for multiple products/markets
- Extension to incorporate additional factors
Examples
--------
Create a basic Bass model for multiple products:
.. plot::
    :context: close-figs
    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    import pymc as pm
    from pymc_marketing.bass.model import create_bass_model
    from pymc_marketing.plot import plot_curve
    from pymc_extras.prior import Prior
    # Create time points - 3 years of monthly data
    n_dates = 12 * 3
    dates = pd.date_range(start="2020-01-01", periods=n_dates, freq="MS")
    t = np.arange(n_dates)
    # Define coordinates for multiple products
    coords = {"T": t, "product": ["A", "B", "C"]}
    # Define priors
    priors = {
        "m": Prior("DiracDelta", c=10_000),  # Market potential
        "p": Prior("Beta", alpha=13.85, beta=692.43, dims="product"),  # Innovation coefficient
        "q": Prior("Beta", alpha=36.2, beta=54.4),  # Imitation coefficient
        "likelihood": Prior("Poisson", dims=("T", "product")),
    }
    # Create the Bass model
    model = create_bass_model(t, observed=None, priors=priors, coords=coords)
    # Sample from the prior predictive distribution
    with model:
        idata = pm.sample_prior_predictive()
    # Plot the adoption curves
    fig, axes = plt.subplots(1, 3, figsize=(10, 6))
    idata.prior["y"].pipe(plot_curve, "T", axes=axes)
    plt.suptitle("Bass Model Prior Predictive Adoption Curves")
    plt.tight_layout()
    plt.show()
"""
from typing import Any
import pymc as pm
import pytensor.tensor as pt
from pymc.model import Model
from pymc_extras.prior import Censored, Prior, VariableFactory, create_dim_handler
[docs]
def F(
    p: float | pt.TensorVariable,
    q: float | pt.TensorVariable,
    t: float | pt.TensorVariable,
) -> pt.TensorVariable:
    r"""Installed base fraction (cumulative adoption proportion).
    This function calculates the cumulative proportion of adopters at time t,
    representing the fraction of the potential market that has adopted the product.
    Parameters
    ----------
    p : float or TensorVariable
        Coefficient of innovation (external influence)
    q : float or TensorVariable
        Coefficient of imitation (internal influence)
    t : array-like or TensorVariable
        Time points
    Returns
    -------
    TensorVariable
        The cumulative proportion of adopters at each time point
    Notes
    -----
    This is the solution to the Bass differential equation:
    .. math::
        F(t) = \frac{1 - e^{-(p+q)t}}{1 + (\frac{q}{p})e^{-(p+q)t}}
    When :math:`t=0`, :math:`F(t)=0`, and as :math:`t` approaches infinity, :math:`F(t)` approaches 1.
    """
    return (1 - pt.exp(-(p + q) * t)) / (1 + (q / p) * pt.exp(-(p + q) * t)) 
[docs]
def f(
    p: float | pt.TensorVariable,
    q: float | pt.TensorVariable,
    t: float | pt.TensorVariable,
) -> pt.TensorVariable:
    r"""Installed base fraction rate of change (adoption rate).
    This function calculates the rate of new adoptions at time t as a
    proportion of the potential market. It represents the probability density
    function of adoption time.
    Parameters
    ----------
    p : float or TensorVariable
        Coefficient of innovation (external influence)
    q : float or TensorVariable
        Coefficient of imitation (internal influence)
    t : array-like or TensorVariable
        Time points
    Returns
    -------
    TensorVariable
        The adoption rate at each time point as a fraction of potential market
    Notes
    -----
    This is the derivative of F(t) with respect to time:
    .. math::
        f(t) = \frac{(p+q)^2 \cdot e^{-(p+q)t}}{p \cdot (1+\frac{q}{p}e^{-(p+q)t})^2}
    Alternatively:
    .. math::
        f(t) = (p + q \cdot F(t)) \cdot (1 - F(t))
    The peak adoption rate occurs at time :math:`t^* = \frac{\ln(q/p)}{p+q}`
    """
    return (p * pt.square(p + q) * pt.exp(t * (p + q))) / pt.square(
        p * pt.exp(t * (p + q)) + q
    ) 
[docs]
def create_bass_model(
    t: pt.TensorLike,
    observed: pt.TensorLike | None,
    priors: dict[str, Prior | Censored | VariableFactory],
    coords: dict[str, Any],
) -> Model:
    r"""Define a Bass diffusion model for product adoption forecasting.
    This function creates a Bayesian Bass diffusion model using PyMC to forecast
    product adoption over time. The Bass model captures both innovation (external
    influence like advertising) and imitation (internal influence like word-of-mouth)
    effects in the adoption process.
    The model includes the following components:
    - Market potential 'm': Total number of eventual adopters
    - Innovation coefficient 'p': Measures external influence
    - Imitation coefficient 'q': Measures internal influence
    - Adopters over time: Number of new adopters at each time point
    - Innovators: Adopters influenced by external factors
    - Imitators: Adopters influenced by previous adopters
    - Peak adoption time: When adoption rate reaches maximum
    Parameters
    ----------
    t : pt.TensorLike
        Time points for which the adoption is modeled.
    observed : pt.TensorLike | None
        Observed adoption data at each time point. If None, only
        prior predictive sampling is possible.
    priors : dict[str, Prior | Censored | VariableFactory]
        Dictionary containing priors for:
        - 'm': Market potential prior
        - 'p': Innovation coefficient prior
        - 'q': Imitation coefficient prior
        - 'likelihood': Observation likelihood model
    coords : dict[str, Any]
        Coordinate values for dimensions in the model, including
        'date' for the time dimension and any other dimensions
        included in the prior specifications.
    Returns
    -------
    Model
        A PyMC model object for the Bass diffusion model, containing
        the variables m, p, q, adopters, innovators, imitators, peak,
        and the likelihood y.
    Notes
    -----
    The returned model can be used for prior predictive checks, posterior
    sampling, and posterior predictive checks to forecast product adoption.
    The model implements the following mathematical relationships:
    .. math::
        \text{adopters}(t) &= m \cdot f(p, q, t) \\
        \text{innovators}(t) &= m \cdot p \cdot (1 - F(p, q, t)) \\
        \text{imitators}(t) &= m \cdot q \cdot F(p, q, t) \cdot (1 - F(p, q, t)) \\
        \text{peak} &= \frac{\ln(q) - \ln(p)}{p + q}
    """
    with pm.Model(coords=coords) as model:
        parameter_dims = (
            set(priors["p"].dims).union(priors["q"].dims).union(priors["m"].dims)
        )
        likelihood_dims = set(getattr(priors["likelihood"], "dims", ()) or ())
        combined_dims = (
            "T",
            *tuple(parameter_dims.union(likelihood_dims).difference(["T"])),
        )
        dim_handler = create_dim_handler(combined_dims)
        m = dim_handler(priors["m"].create_variable("m"), priors["m"].dims)
        p = dim_handler(priors["p"].create_variable("p"), priors["p"].dims)
        q = dim_handler(priors["q"].create_variable("q"), priors["q"].dims)
        time = dim_handler(t, "T")
        adopters = pm.Deterministic("adopters", m * f(p, q, time), dims=combined_dims)
        pm.Deterministic(
            "innovators",
            m * p * (1 - F(p, q, time)),
            dims=combined_dims,
        )
        pm.Deterministic(
            "imitators",
            m * q * F(p, q, time) * (1 - F(p, q, time)),
            dims=combined_dims,
        )
        peak = (pt.log(q) - pt.log(p)) / (p + q)
        peak_dims = tuple(parameter_dims) if parameter_dims else None
        pm.Deterministic("peak", peak, dims=peak_dims)
        priors["likelihood"].dims = combined_dims
        priors["likelihood"].create_likelihood_variable(  # type: ignore
            "y",
            mu=adopters,
            observed=observed,
        )
    return model