QFlex quantile-parameterized distributions with flexible basis functions
Project description
QFlex
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a48aa0341b4a487b7a5684f40a03369ac4632b9e77882dbda78e76d86efda207
|
|
| MD5 |
01bd40fdf8b8af11d300ad8ef1aeb303
|
|
| BLAKE2b-256 |
a7cb89baad8bdfa4fa3713d47bc08d618366cf3d6cf542deb49ffb1d3dadd503
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b21f818bbd0d8b208a1a657d10b3852ba3406a2b72b81544a31386300254d162
|
|
| MD5 |
6672b553622f80553e352055d086a4ea
|
|
| BLAKE2b-256 |
a18c69d1054cfc16a22ac4a15469dda42b867c0968a4dc73325eed23029fdc3b
|