Metadata-Version: 2.4
Name: qflex
Version: 1.0.0
Summary: QFlex quantile-parameterized distributions with flexible basis functions
Project-URL: Repository, https://github.com/rohitkhanna/qflex
License: MIT License
        
        Copyright (c) 2026 Rohit Khanna
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: distribution,metalog,probability,quantile,statistics
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.9
Requires-Dist: numpy>=1.24
Requires-Dist: scipy>=1.10
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: twine>=4.0; extra == 'dev'
Provides-Extra: linear
Requires-Dist: cvxpy>=1.3; extra == 'linear'
Description-Content-Type: text/markdown

# QFlex

[![PyPI version](https://badge.fury.io/py/qflex.svg)](https://badge.fury.io/py/qflex)
[![Python](https://img.shields.io/pypi/pyversions/qflex.svg)](https://pypi.org/project/qflex/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/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

```bash
pip install qflex
```

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

```bash
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)

```python
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)

```python
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

```python
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).

```python
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)`.

```python
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))`.

```python
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 |

```python
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

```python
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](LICENSE) for details.
