Creating Custom Statistical Distributions in scipy.stats.rv_continuous

Creating Custom Statistical Distributions in scipy.stats.rv_continuous

The scipy.stats.rv_continuous class is a powerful tool in the SciPy library that allows users to create and work with continuous random variable distributions. A continuous random variable is one that can take an infinite number of possible values within a specified range. This class is particularly useful for statisticians, data scientists, and anyone who needs to model and analyze data where the underlying distributions are not standard or well-known.

By subclassing rv_continuous, users can define their own custom probability density functions (PDFs), cumulative distribution functions (CDFs), and other statistical functions. This flexibility makes it possible to accurately represent complex real-world phenomena and perform simulations or calculations that are tailored to specific problems.

One of the key features of rv_continuous is its ability to handle both analytical and numerical methods. For distributions that have a known closed-form, users can directly specify the PDF and CDF. However, for more complex distributions without a closed-form solution, rv_continuous can numerically approximate the necessary functions. This hybrid approach ensures that the class can be used in a wide variety of scenarios.

To begin using rv_continuous, one must first import it from the scipy.stats module as follows:

from scipy.stats import rv_continuous

After importing the class, the next step is to define a new distribution by creating a subclass of rv_continuous. This involves implementing methods that define the custom PDF and potentially other functions such as the CDF, survival function, and quantile function. Once these methods are in place, the new distribution can be used just like any built-in distribution provided by SciPy.

In the following sections, we will explore how to define custom PDFs, implement custom random number generators (RNGs), estimate parameters for custom distributions, and test and validate these custom distributions to ensure they meet the required statistical properties and behaviors.

Defining Custom Probability Density Functions (PDFs)

To define a custom PDF, we need to override the _pdf method in our subclass. This method takes two arguments: x, which represents the value at which the PDF is evaluated, and any additional parameters the distribution may have. The method should return the value of the PDF at x. For example, let’s create a simple custom distribution with a triangular PDF:

class TriangularDistribution(rv_continuous):
    def _pdf(self, x, c):
        if x  1:
            return 0
        elif x < c:
            return 2 * x / c
        else:
            return 2 * (1 - x) / (1 - c)

In this example, c is an additional parameter that defines the mode of the triangular distribution (the peak of the triangle). It must be between 0 and 1. Values outside the range [0, 1] are assigned a probability of 0, as they are not valid for this distribution.

Once we have defined our custom PDF, we can create an instance of our distribution and use it to calculate probabilities or generate random samples:

tri_dist = TriangularDistribution(name='triangular', a=0, b=1)
print(tri_dist.pdf(0.5, c=0.25))  # Evaluate PDF at x=0.5 with c=0.25

It’s important to note that rv_continuous provides default implementations for other related methods like the CDF (_cdf) and inverse CDF (_ppf), which are numerically calculated based on the PDF. However, if we have an analytical expression for these functions, we can also override these methods for improved performance:

def _cdf(self, x, c):
    if x < 0:
        return 0
    elif x < c:
        return (x**2) / c
    elif x <= 1:
        return 1 - ((1 - x)**2) / (1 - c)
    else:
        return 1

Defining a custom CDF can be particularly useful when we need to perform operations that require its inverse, such as generating random samples using the inverse transform sampling method.

In some cases, we may also want to define the survival function (_sf) or the moment-generating function (_mgf). These are not required but can be useful for certain types of analysis or optimization problems.

Lastly, it’s essential to validate our custom PDF to ensure it’s correctly normalized. The integral of the PDF over its domain should be equal to 1. We can use SciPy’s integration functions to verify this:

from scipy.integrate import quad

def check_normalization(dist, params):
    result, _ = quad(lambda x: dist.pdf(x, *params), dist.a, dist.b)
    return result

print(check_normalization(tri_dist, (0.25,)))  # Should be close to 1

In summary, defining custom PDFs using rv_continuous involves subclassing the class and implementing the _pdf method (and potentially other related methods) with our custom probability density function. Once defined, we can use our custom distribution just like any built-in distribution in SciPy.

Implementing Custom Random Number Generators (RNGs)

After defining custom PDFs, the next logical step is to implement custom random number generators (RNGs). RNGs are important for simulation and sampling, enabling us to generate random values that follow our newly defined distribution. In scipy.stats.rv_continuous, this can be done by overriding the _rvs method.

The _rvs method is responsible for generating random variates (values) that adhere to the distribution’s PDF. When creating a custom RNG, you have the option to utilize external libraries like NumPy or write your own generator from scratch. Here is an example using NumPy’s random functions to generate random variates for our triangular distribution:

import numpy as np

class TriangularDistribution(rv_continuous):
    # ... (other methods) ...
    
    def _rvs(self, c):
        u = self._random_state.uniform(0, 1, size=self._size)
        return np.where(u < c, np.sqrt(u * c), 1 - np.sqrt((1 - u) * (1 - c)))

In the above code snippet, u is an array of uniform random numbers between 0 and 1. The np.where function applies the inverse CDF of the triangular distribution to transform these uniform variates into triangular-distributed variates. That is a common technique known as inverse transform sampling.

This customized RNG can then be used to generate random samples as shown below:

tri_dist = TriangularDistribution(name='triangular', a=0, b=1)
samples = tri_dist.rvs(c=0.25, size=1000)

Here, samples is an array of 1000 random values drawn from the triangular distribution with a mode at c=0.25. These samples can be used for further statistical analysis or simulations.

It is important to validate the RNG by checking if the generated samples follow the desired distribution. This can be done visually using plots or quantitatively using statistical tests, which we will cover in the testing and validation subsection.

Overall, implementing custom RNGs in scipy.stats.rv_continuous allows for a high degree of control over the random sampling process and enables users to extend the functionality of SciPy to suit their unique requirements.

Estimating Parameters for Custom Distributions

When it comes to estimating parameters for custom distributions, there are a few techniques that can be employed depending on the complexity of the distribution and the data available. One common approach is to use maximum likelihood estimation (MLE), which involves finding the parameter values that maximize the likelihood of observing the given data. Another approach is the method of moments, where parameters are estimated by equating sample moments (e.g., mean, variance) with their theoretical counterparts.

Let’s say we have a set of observed data points and we want to fit our TriangularDistribution to this data. We can use the fit method provided by rv_continuous, which by default uses MLE to estimate the parameters. Here’s an example:

import numpy as np
from scipy.stats import rv_continuous

class TriangularDistribution(rv_continuous):
    # ... (PDF and CDF definitions as above)

# Generate some synthetic data from a Triangular distribution
np.random.seed(123)
data = np.random.triangular(left=0, mode=0.25, right=1, size=1000)

# Fit our custom distribution to the data
tri_dist = TriangularDistribution(name='triangular', a=0, b=1)
params = tri_dist.fit(data)
print(f"Estimated parameters: c={params[-1]}")

The fit method returns a tuple of parameter estimates, including loc and scale parameters, which are used for shifting and scaling the distribution, respectively. Since our TriangularDistribution has only one shape parameter c, it’s the last element in the returned tuple.

It’s also possible to provide initial guesses for the parameters and to fix certain parameters during the fitting process. This can be useful if we have prior knowledge about the distribution or if we want to compare nested models. For example:

# Fit with an initial guess for c and fixed loc and scale
c_guess = 0.3
fixed_params = {'floc': 0, 'fscale': 1}
params = tri_dist.fit(data, c_guess, **fixed_params)
print(f"Estimated parameters with fixed loc and scale: c={params[-1]}")

In practice, it is important to assess the quality of the fit by looking at goodness-of-fit tests, such as the Kolmogorov-Smirnov test, and by visualizing the fitted distribution against the histogram of observed data. This helps ensure that the estimated parameters lead to a distribution that adequately captures the underlying data-generating process.

Finally, if the built-in fitting methods are not suitable or if you need more control over the estimation process, you can implement your own parameter estimation routine using optimization libraries like scipy.optimize.

Testing and Validating Custom Distributions

Now that we have defined our custom probability density function and possibly other related functions, it’s important to test and validate the custom distribution to ensure it behaves as expected. This step is essential to confirm that our distribution meets all the statistical properties and accurately models the real-world phenomenon we are interested in.

One way to validate our custom distribution is by plotting its PDF and comparing it visually to the expected shape. We can use libraries such as matplotlib to generate these plots:

import matplotlib.pyplot as plt
import numpy as np

# Create an instance of the custom distribution
tri_dist = TriangularDistribution(name='triangular', a=0, b=1)

# Generate values within the range
x_values = np.linspace(0, 1, 100)
# Calculate the PDF for each x value
pdf_values = tri_dist.pdf(x_values, c=0.25)

# Plot the PDF
plt.plot(x_values, pdf_values)
plt.title('PDF of Custom Triangular Distribution')
plt.xlabel('x')
plt.ylabel('PDF')
plt.show()

Another important aspect of validation is to ensure that our distribution is properly normalized. The integral of the PDF over the entire range should equal 1. We can check this using numerical integration:

from scipy.integrate import quad

# Define the PDF as a standalone function for integration
def triangular_pdf(x, c):
    if x  1:
        return 0
    elif x < c:
        return 2 * x / c
    else:
        return 2 * (1 - x) / (1 - c)

# Numerically integrate the PDF over the range [0, 1]
integral_result, _ = quad(triangular_pdf, 0, 1, args=(0.25,))
print(f'Integral of PDF: {integral_result}')  # Should be close to 1

Additionally, we can validate our custom distribution by generating a large number of random samples and comparing the empirical distribution to the theoretical one. This can be done by plotting a histogram of the samples and overlaying the theoretical PDF:

# Generate a large number of random samples
samples = tri_dist.rvs(c=0.25, size=10000)

# Plot a histogram of the samples
plt.hist(samples, bins=50, density=True, alpha=0.5, label='Empirical')

# Overlay the theoretical PDF
plt.plot(x_values, pdf_values, label='Theoretical')

plt.title('Empirical vs Theoretical Distribution')
plt.xlabel('x')
plt.ylabel('Frequency')
plt.legend()
plt.show()

Finally, we can perform statistical tests such as the Kolmogorov-Smirnov test to quantitatively assess how well our samples match the theoretical distribution:

from scipy.stats import kstest

# Perform the Kolmogorov-Smirnov test
ks_statistic, p_value = kstest(samples, tri_dist.cdf, args=(0.25,))
print(f'KS Statistic: {ks_statistic}')
print(f'P-value: {p_value}')  # A large p-value suggests a good fit

By performing these tests and validations, we can gain confidence in our custom distribution’s accuracy and reliability for further analysis and simulations.

Source: https://www.pythonlore.com/creating-custom-statistical-distributions-in-scipy-stats-rv_continuous/


You might also like this video