Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"mypy>=1.0.0",
"ruff>=0.8.0",
]
docs = [
"sphinx>=8.0.0; python_version >= '3.10'",
Expand Down Expand Up @@ -80,3 +81,32 @@ exclude_lines = [
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
]

[tool.ruff]
target-version = "py38"
line-length = 88

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"UP", # pyupgrade
"B", # flake8-bugbear
"RUF", # ruff-specific rules
"NPY", # numpy-specific rules
"C4", # flake8-comprehensions
]
ignore = [
"E501", # line length handled by ruff format
"RUF001", # ambiguous unicode in strings (intentional math symbols: γ, ξ)
"RUF002", # ambiguous unicode in docstrings
]

[tool.ruff.lint.per-file-ignores]
"src/tailestim/estimators/plot/*" = ["F841", "E741"]
"tests/*" = ["NPY002"]

[tool.ruff.lint.isort]
known-first-party = ["tailestim"]
24 changes: 11 additions & 13 deletions src/tailestim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@
# SPDX-License-Identifier: MIT

from .datasets import TailData

from .estimators.base import BaseTailEstimator
from .estimators.estimator_set import TailEstimatorSet
from .estimators.hill import HillEstimator
from .estimators.moments import MomentsEstimator
from .estimators.kernel import KernelTypeEstimator
from .estimators.moments import MomentsEstimator
from .estimators.pickands import PickandsEstimator
from .estimators.smooth_hill import SmoothHillEstimator

from .estimators.estimator_set import TailEstimatorSet

__all__ = [
'TailData',
'BaseTailEstimator',
'HillEstimator',
'MomentsEstimator',
'KernelTypeEstimator',
'PickandsEstimator',
'SmoothHillEstimator',
'TailEstimatorSet',
]
"BaseTailEstimator",
"HillEstimator",
"KernelTypeEstimator",
"MomentsEstimator",
"PickandsEstimator",
"SmoothHillEstimator",
"TailData",
"TailEstimatorSet",
]
44 changes: 24 additions & 20 deletions src/tailestim/datasets.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import logging
import os

import numpy as np
import logging


class TailData:
"""Load and manage tail distribution datasets.

This class provides functionality to load datasets either from the package's
built-in data directory using a name, or from a custom path provided by the user.

Parameters
----------
name : str, optional
Expand All @@ -16,7 +18,7 @@ class TailData:
path : str, optional
Path to a custom dataset file. If provided, this takes precedence over `name`.
Must be provided if `name` is None.

Attributes
----------
name : str or None
Expand All @@ -25,39 +27,39 @@ class TailData:
Path to the dataset file if a custom dataset was loaded.
data : numpy.ndarray
The loaded dataset as a numpy array.

Examples
--------
Load a built-in dataset:

>>> data = TailData(name='CAIDA_KONECT')
>>> print(len(data.data))

Load a custom dataset:

>>> data = TailData(path='path/to/my/data.dat')
>>> print(len(data.data))
"""

def __init__(self, name=None, path=None):
if name is None and path is None:
raise ValueError("Either 'name' or 'path' must be provided")

if name is not None and path is not None:
logging.info("Both 'name' and 'path' provided; 'path' will take precedence")

self.name = name
self.path = path
self.data = self.load_data()

def load_data(self):
"""Load data from either a built-in dataset or a custom file path.

Returns
-------
numpy.ndarray
The loaded dataset as a numpy array.

Raises
------
FileNotFoundError
Expand All @@ -70,20 +72,22 @@ def load_data(self):
logging.info(f"Using custom path: {file_path}")
else:
# Use the package data directory with the provided name
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
file_path = os.path.join(data_dir, f'{self.name}.dat')
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
file_path = os.path.join(data_dir, f"{self.name}.dat")
logging.info(f"Using package data path: {file_path}")

# Check if the file exists
if not os.path.exists(file_path):
if self.path is not None:
raise FileNotFoundError(f"Data file not found at path: {file_path}")
else:
raise FileNotFoundError(f"Data file '{self.name}.dat' not found in package data directory.")
raise FileNotFoundError(
f"Data file '{self.name}.dat' not found in package data directory."
)

# Load the data from the file using the provided method
logging.info(f"Loading data from file: {file_path}")
with open(file_path, 'r') as file:
with open(file_path) as file:
lines = file.readlines()

# Determine the total number of data points
Expand All @@ -94,14 +98,14 @@ def load_data(self):
# Populate the ordered_data array
for line in lines:
degree, count = line.strip().split()
ordered_data[current_index:current_index + int(count)] = float(degree)
ordered_data[current_index : current_index + int(count)] = float(degree)
current_index += int(count)

return ordered_data

def __repr__(self):
"""Return a string representation of the TailData object.

Returns
-------
str
Expand All @@ -110,4 +114,4 @@ def __repr__(self):
if self.path is not None:
return f"TailData(path='{self.path}', data_length={len(self.data)})"
else:
return f"TailData(name='{self.name}', data_length={len(self.data)})"
return f"TailData(name='{self.name}', data_length={len(self.data)})"
18 changes: 9 additions & 9 deletions src/tailestim/estimators/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"""Estimators for tail index estimation."""

from .base import BaseTailEstimator
from .estimator_set import TailEstimatorSet
from .hill import HillEstimator
from .moments import MomentsEstimator
from .kernel import KernelTypeEstimator
from .moments import MomentsEstimator
from .pickands import PickandsEstimator
from .smooth_hill import SmoothHillEstimator
from .estimator_set import TailEstimatorSet

__all__ = [
'BaseTailEstimator',
'HillEstimator',
'MomentsEstimator',
'KernelTypeEstimator',
'PickandsEstimator',
'SmoothHillEstimator',
'TailEstimatorSet',
"BaseTailEstimator",
"HillEstimator",
"KernelTypeEstimator",
"MomentsEstimator",
"PickandsEstimator",
"SmoothHillEstimator",
"TailEstimatorSet",
]
57 changes: 31 additions & 26 deletions src/tailestim/estimators/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"""Base class for tail index estimation."""
import numpy as np

from abc import ABC, abstractmethod
from typing import Dict, Any, Tuple, Union
from numpy.random import BitGenerator, SeedSequence, RandomState, Generator
from typing import Any, Dict, Tuple, Union

import numpy as np
from numpy.random import BitGenerator, Generator, RandomState, SeedSequence

from .result import TailEstimatorResult


class BaseTailEstimator(ABC):
"""Abstract base class for tail index estimation.

Expand All @@ -22,12 +26,15 @@ class BaseTailEstimator(ABC):
**kwargs : dict
Additional parameters specific to each estimation method.
"""

def __init__(
self,
bootstrap: bool = True,
base_seed: Union[None, SeedSequence, BitGenerator, Generator, RandomState] = None,
**kwargs):
self,
bootstrap: bool = True,
base_seed: Union[
None, SeedSequence, BitGenerator, Generator, RandomState
] = None,
**kwargs,
):
self.bootstrap = bootstrap
self.base_seed = base_seed
self.kwargs = kwargs
Expand All @@ -36,12 +43,12 @@ def __init__(
@abstractmethod
def _estimate(self, ordered_data: np.ndarray) -> Tuple:
"""Core estimation method to be implemented by each specific estimator.

Parameters
----------
ordered_data : np.ndarray
Data array in decreasing order.

Returns
-------
Tuple
Expand All @@ -51,34 +58,32 @@ def _estimate(self, ordered_data: np.ndarray) -> Tuple:

def fit(self, data: np.ndarray) -> None:
"""Fit the estimator to the data.

Parameters
----------
data : np.ndarray
Input data array (e.g., degree sequence). The data will automatically be sorted in decreasing order.
"""
ordered_data = np.sort(data)[::-1] # Each estimating functions require the data to be in decreasing order
ordered_data = np.sort(data)[
::-1
] # Each estimating functions require the data to be in decreasing order
self.results = self._estimate(ordered_data)

@abstractmethod
def get_params(self) -> Dict[str, Any]:
"""Get the parameters of the estimator.

Returns
-------
dict
Dictionary containing the parameters of the estimator.
"""
return {
"bootstrap": self.bootstrap,
"base_seed": self.base_seed,
**self.kwargs
}
return {"bootstrap": self.bootstrap, "base_seed": self.base_seed, **self.kwargs}

@abstractmethod
def get_result(self) -> TailEstimatorResult:
"""Get the estimated parameters.

Returns
-------
TailEstimatorResult
Expand All @@ -96,7 +101,7 @@ def get_result(self) -> TailEstimatorResult:
if self.results is None:
raise ValueError("Model not fitted yet. Call fit() first.")
return TailEstimatorResult()

def __repr__(self) -> str:
"""Return a string representation of the estimator."""
return f"{self.__class__.__name__}(bootstrap={self.bootstrap}, base_seed={self.base_seed}, kwargs={self.kwargs})"
Expand All @@ -108,19 +113,19 @@ def __str__(self) -> str:
estim_str += f"Estimator Type: {self.__class__.__name__}\n"
estim_str += "-" * 50 + "\n"
estim_str += f"Fitted: {'Yes' if self.results is not None else 'No'}\n"

# Add the arguments provided
estim_str += "Arguments:\n"
estim_str += f" bootstrap: {self.bootstrap}\n"
estim_str += f" base_seed: {self.base_seed}\n"

# Add any additional kwargs
if self.kwargs:
for key, value in self.kwargs.items():
estim_str += f" {key}: {value}\n"
# If the model is not fitted, return just the estim_str

# If the model is not fitted, return just the estim_str
if self.results is None:
return estim_str + "Model not fitted yet. Call fit() first."
return estim_str

return estim_str
Loading
Loading