Skip to main content

QFlex quantile-parameterized distributions with flexible basis functions

Project description

QFlex

PyPI version Python License: MIT

A Python library implementing QFlex quantile-parameterized distributions — a flexible family of probability distributions fit directly from quantile data, with support for unbounded, semibounded, and bounded domains.


Installation

pip install qflex

To enable the linear Proposition 5 solver (requires CVXPY):

pip install qflex[linear]

Background and Theory

What is QFlex?

QFlex is a family of quantile-parameterized distributions (QPDs) that represents a probability distribution through its quantile function Q(p) rather than through a PDF or CDF. This makes it especially suited for:

  • Expert elicitation — fit a distribution directly from quantile assessments (e.g. P10, P50, P90)
  • Bayesian updating — update quantile-based priors without needing closed-form conjugates
  • Flexible tail modelling — the basis structure independently controls left tail, right tail, and center behaviour

The Quantile Function

The QFlex quantile function is a linear combination of three families of basis functions:

Q(p) = Σ_{j=1}^{m} [ a_j · R_j(p)  +  b_j · L_j(p)  +  c_j · C_j(p) ]

where the three basis families are:

Family Formula Role
Right tail R_j(p) [-ln(1-p)]^j Controls right (upper) tail behaviour
Left tail L_j(p) (-1)^(j+1) · [ln(p)]^j Controls left (lower) tail behaviour
Center C_j(p) (p - γ)^(2j-1) Controls centre/body behaviour

The total number of coefficients is determined by terms, which sets the depth of each family.

The Gamma (γ) Parameter

γ is an internal location parameter that shifts the centre basis to match the skewness of the data. It is estimated automatically from the empirical P10, P50, and P90 of the input:

γ = (P50 - P10) / (P90 - P10)

A symmetric distribution gives γ ≈ 0.5. Right-skewed data gives γ < 0.5 and left-skewed gives γ > 0.5.

PDF and Feasibility

The PDF is obtained from the quantile function via:

f(Q(p)) = 1 / q(p)     where     q(p) = dQ/dp

A valid distribution requires q(p) > 0 for all p ∈ (0,1), i.e. Q(p) must be strictly increasing. This is not guaranteed by unconstrained least-squares fitting, which motivates the constraint system.

Constraints (Propositions 3–5)

The paper establishes sufficient conditions for feasibility:

  • Proposition 3 (A / A+): Restricting all non-intercept coefficients to be non-negative guarantees a valid PDF.
  • Proposition 4 (TC_MAG): A grid-based condition requiring the minimum derivative contribution from the tail bases to exceed the maximum contribution from the centre basis: m_tail > M_center.
  • Proposition 5 (TC): A sharper analytical condition on the ratio of tail-to-centre basis magnitudes, enforced via SLSQP (nonlinear) or a linear auxiliary-variable reformulation.

The constraints form a hierarchy from most restrictive (A) to least restrictive (TC), with TC closest to the true feasibility boundary.

Bounded and Semibounded Variants

For data that cannot be negative or must lie within a finite range, QFlex is applied to a transformed space:

Variant Transform Use case
LogQFlex z = ln(x - L) x ∈ (L, +∞) — income, prices, durations
LogitQFlex z = ln((x-L)/(U-x)) x ∈ (L, U) — proportions, scores, rates

Fitting happens on z; all outputs are mapped back to the original x scale.


Quick Start

From quantile pairs (expert elicitation)

import numpy as np
from qflex import QFlex, LogQFlex, LogitQFlex, ConstraintType

# Quantile assessments from an expert or empirical summary
y_data = [0.10, 0.25, 0.50, 0.75, 0.90]   # cumulative probabilities
x_data = [12.0, 18.0, 25.0, 34.0, 45.0]   # corresponding quantile values

qf = QFlex(x_data, y_data, terms=5)

print(qf.quantile([0.1, 0.5, 0.9]))        # → [12.0, 25.0, 45.0]
print(qf.pdf([0.1, 0.5, 0.9]))             # density at those quantile points
print(qf.cdf([20.0, 25.0, 30.0]))          # CDF at x values

samples = qf.sample(size=1000)

m = qf.moments(order=4)
print(m['mean'], m['std'], m['skewness'], m['kurtosis'])

From raw data (Weibull plotting positions)

import numpy as np
from qflex import QFlex, LogQFlex, LogitQFlex

data = np.random.lognormal(mean=3, sigma=0.5, size=200)

# Sorts data, assigns y_i = i/(n+1), then fits
qf       = QFlex.fit_from_data(data, terms=5)
log_qf   = LogQFlex.fit_from_data(data, lower_bound=0, terms=5)

Summarise and plot

qf.summary()        # prints formatted table of moments, percentiles, coefficients

fig, axes = qf.plot()          # PDF (left) + quantile function (right)
fig.savefig('fit.png')

Distribution Variants

Unbounded: QFlex

For data with no natural bounds (e.g. log-returns, temperature anomalies).

qf = QFlex(x_data, y_data, terms=5)

Semibounded: LogQFlex

For data with a lower bound (e.g. income, time-to-event, asset prices). Internally fits QFlex to ln(x - lower_bound).

qf = LogQFlex(x_data, y_data, lower_bound=0, terms=5)

Bounded: LogitQFlex

For data bounded on both sides (e.g. proportions, percentages, test scores). Internally fits QFlex to logit((x - L) / (U - L)).

qf = LogitQFlex(x_data, y_data, lower_bound=0, upper_bound=1, terms=5)

Constraint Types

Constraint Description Restrictiveness
ConstraintType.NONE Unconstrained least squares (default)
ConstraintType.A All coefficients ≥ 0 for k ≥ 2 (Prop 3) Most restrictive
ConstraintType.TL Leading tail coefficients ≥ 0 High
ConstraintType.TA All tail coefficients ≥ 0 Medium
ConstraintType.TC Prop 5 tail-centre margin > 0 via SLSQP Low
ConstraintType.TC_MAG Prop 4 grid-based m_tail > M_center Least restrictive
qf = QFlex(x_data, y_data, terms=5, constraint_type=ConstraintType.TC_MAG)

# TC with linear reformulation (requires cvxpy)
qf = QFlex(x_data, y_data, terms=5,
           constraint_type=ConstraintType.TC, tc_method='linear')

API Reference

All three classes (QFlex, LogQFlex, LogitQFlex) share the following interface:

Constructor

Class Signature
QFlex QFlex(x_data, y_data, terms=5, constraint_type=..., tc_method='nonlinear')
LogQFlex LogQFlex(x_data, y_data, lower_bound, terms=5, ...)
LogitQFlex LogitQFlex(x_data, y_data, lower_bound, upper_bound, terms=5, ...)

Instance Methods

Method Input Output Notes
quantile(y) y ∈ (0,1) x values Core quantile function Q(p)
pdf(y, method='numerical') y ∈ (0,1) density values Use method='analytical' for closed-form (QFlex only)
cdf(x) x values p ∈ (0,1) Inverts Q(p) numerically
sample(size=1) int np.ndarray Inverse transform sampling
moments(order=4) int dict Keys: mean, variance, std, skewness, kurtosis, raw_k, central_k
summary() printed table Terms, γ, feasibility, moments, P10/P50/P90, coefficients
plot(p_grid, show_data, ax) optional (fig, axes) PDF panel + quantile function panel
check_proposition4() dict Keys: satisfied, m_tail, M_center, margin, q_flex_min, q_flex_positive

Class Method

Method Description
fit_from_data(data, terms=5, constraint_type=..., **kwargs) Fit from raw observations using Weibull plotting positions y_i = i/(n+1)

Utility

from qflex.utils import compute_w1

w1, w1_norm = compute_w1(qf.quantile, x_data, y_data)
# w1       → Wasserstein-1 distance between fitted and target quantile functions
# w1_norm  → W1 normalised by (P90 - P10) of the data

Reference

⚠️ TODO — fill in before publishing:

[Author(s)]. "[Paper Title]." Journal/Conference, vol. X, no. Y, Year, pp. Z–Z. DOI: [doi link]

This implementation follows the basis functions, gamma estimation, and Propositions 3–5 as described in the paper above.


License

MIT License. See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qflex-1.0.0.tar.gz (22.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

qflex-1.0.0-py3-none-any.whl (26.5 kB view details)

Uploaded Python 3

File details

Details for the file qflex-1.0.0.tar.gz.

File metadata

  • Download URL: qflex-1.0.0.tar.gz
  • Upload date:
  • Size: 22.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for qflex-1.0.0.tar.gz
Algorithm Hash digest
SHA256 a48aa0341b4a487b7a5684f40a03369ac4632b9e77882dbda78e76d86efda207
MD5 01bd40fdf8b8af11d300ad8ef1aeb303
BLAKE2b-256 a7cb89baad8bdfa4fa3713d47bc08d618366cf3d6cf542deb49ffb1d3dadd503

See more details on using hashes here.

File details

Details for the file qflex-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: qflex-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 26.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for qflex-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b21f818bbd0d8b208a1a657d10b3852ba3406a2b72b81544a31386300254d162
MD5 6672b553622f80553e352055d086a4ea
BLAKE2b-256 a18c69d1054cfc16a22ac4a15469dda42b867c0968a4dc73325eed23029fdc3b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page