diff --git a/lrmodule/input_data.py b/lrmodule/input_data.py index 114b5e3..fc96420 100644 --- a/lrmodule/input_data.py +++ b/lrmodule/input_data.py @@ -1,12 +1,14 @@ -from collections import defaultdict -from collections.abc import Iterator +import logging +from collections.abc import Iterable from enum import StrEnum from functools import cache from pathlib import Path -import pandas as pd -from lir.data.data_strategies import DataStrategy -from lir.data.models import FeatureData +from lir.data.io import search_path +from lir.data.models import DataStrategy, FeatureData +from lir.datasets.feature_data_csv import ExtraField, FeatureDataCsvParser + +LOG = logging.getLogger(__name__) class TestTrainSplit(StrEnum): @@ -15,16 +17,29 @@ class TestTrainSplit(StrEnum): TEST = "v" -class ScratchData(DataStrategy): - def __init__(self, input_file_path: Path): +SPLIT_COLUMNS = ["split1", "split2", "split3"] + + +class ScratchCsvReader(FeatureDataCsvParser): + def __init__(self, input_file_path: Path | str): """Read and represent Scratch specific input data, as corresponding instances. The data might include n-fold cross validation splits, where each fold has a train/test split. This class provides access to iterate over the available folds and the corresponding train and test splits. """ - self.file_path = input_file_path + super().__init__( + source_id_column=["weapon1", "weapon2"], + label_column="hypothesis", + extra_fields=[ + ExtraField("split", SPLIT_COLUMNS, str), + ], + message_prefix=f"{input_file_path}: ", + ) + + self.file_path = Path(input_file_path) - def _read_instances_from_file(self): + @cache + def get_instances(self) -> FeatureData: """Read K-fold cross validation CSV input data to a list of K corresponding subsets of test/train folds. In the CSV file, subsets of the data are indicated by the "split" column. For example, 3-fold cross @@ -36,58 +51,18 @@ def _read_instances_from_file(self): The remaining columns are treated as features. This means that the pipeline in which this data is used should filter out any non-relevant feature columns before training or evaluating a model. """ - df = pd.read_csv(self.file_path) - - # Ensure all expected columns are present - expected_columns = ["weapon1", "weapon2", "hypothesis", "split1"] - if not all(column in df.columns for column in expected_columns): - raise ValueError( - f"Missing one of the expected columns: {', '.join(set(expected_columns) - set(df.columns))}" - ) - - # Find all columns regarding the prepared folds, named 'split*' ('split1', 'split2', etc.) - fold_column_names = [c for c in df.columns if c.startswith("split")] + path = search_path(self.file_path) + LOG.debug(f"parsing CSV file: {self.file_path} as {path}") + with open(path) as f: + return self._parse_file(f) - # Feature columns are all columns that are not the expected columns - feature_columns = [c for c in df.columns if c not in expected_columns and c not in fold_column_names] - label_column = ["hypothesis"] - - # Group the folds by the column name, i.e. 'split1', 'split2', etc. - df_with_subsets = df.melt( - id_vars=label_column + feature_columns, - value_vars=fold_column_names, - var_name="subset", - value_name="test_train_split", - ) - - subsets = [] - - # Loop over each subset - for _, folds in df_with_subsets.groupby("subset"): - # Filter out the data marked as "not used" - test_train_folds = folds[folds.test_train_split != TestTrainSplit.NOT_USED] - - # Loop over 'train' / 'test' folds for the current subset - subset_folds = defaultdict() - - for test_or_train_indicator, raw_data in test_train_folds.groupby("test_train_split"): - # The `test_or_train_indicator` refers to the role of this data - # in the current fold; belonging to either the 'test' or 'train' split. - features = raw_data[feature_columns].to_numpy(dtype=float).reshape(-1, len(feature_columns)) - labels = raw_data[label_column].to_numpy(dtype=int).flatten() - - subset_folds[test_or_train_indicator] = FeatureData(features=features, labels=labels) - - subsets.append((subset_folds[TestTrainSplit.TRAIN], subset_folds[TestTrainSplit.TEST])) - - return subsets - - @cache - def _get_instances(self): - """Read instances from file only once.""" - return self._read_instances_from_file() +class PredefinedCrossValidation(DataStrategy): + """Return a series of train/test sets for a predefined cross-validation setup.""" - def __iter__(self) -> Iterator[tuple[FeatureData, FeatureData]]: - """Allow iteration by looping over the resulting train/test split(s).""" - yield from self._get_instances() + def apply(self, instances: FeatureData) -> Iterable[tuple[FeatureData, FeatureData]]: + """Return a series of train/test sets for a predefined cross-validation setup.""" + for split in range(len(SPLIT_COLUMNS)): + training_data = instances[instances.split[:, split] == TestTrainSplit.TRAIN.value] # type: ignore + test_data = instances[instances.split[:, split] == TestTrainSplit.TEST.value] # type: ignore + yield training_data, test_data diff --git a/lrmodule/mcmc.py b/lrmodule/mcmc.py deleted file mode 100644 index 7a6b4c5..0000000 --- a/lrmodule/mcmc.py +++ /dev/null @@ -1,203 +0,0 @@ -from typing import Self - -import arviz as az -import numpy as np -import pymc as pm -from lir.algorithms.bayeserror import ELUBBounder -from lir.bounding import LLRBounder -from lir.data.models import FeatureData, LLRData -from lir.transform import Transformer -from scipy.stats import betabinom, binom, norm - - -class McmcLLRModel(Transformer): - """ - Use Markov Chain Monte Carlo simulations to fit a statistical distribution for each of the two hypotheses. - - Using samples from the posterior distributions of the model parameters, a posterior distribution of the LR is - obtained. The median of this distribution is used as best estimate for the LR; a credible interval is also - determined. - """ - - def __init__( # noqa: PLR0913 (more than five arguments are allowed) - self, - distribution_h1: str, - parameters_h1: dict[str, dict[str, int]] | None, - distribution_h2: str, - parameters_h2: dict[str, dict[str, int]] | None, - bounding: LLRBounder | None = ELUBBounder(), - interval: tuple[float, float] = (0.05, 0.95), - **mcmc_kwargs, - ): - """ - Initialise the MCMC model, based on distributions and parameters. - - :param distribution_h1: statistical distribution used to model H1, for example 'normal' or 'binomial' - :param parameters_h1: definition of the parameters of distribution_h1, and their prior distributions - :param distribution_h2: statistical distribution used to model H2, for example 'normal' or 'binomial' - :param parameters_h2: definition of the parameters of distribution_h2, and their prior distributions - :param bounder: bounding method to apply to the unbound llrs, to prevent overextrapolation - :param interval: lower and upper bounds of the credible interval in range 0..1; default: (0.05, 0.95) - :param mcmc_kwargs: mcmc simulation settings, see `McmcModel.__init__` for more details. - """ - self.model_h1 = McmcModel(distribution_h1, parameters_h1, **mcmc_kwargs) - self.model_h2 = McmcModel(distribution_h2, parameters_h2, **mcmc_kwargs) - self.bounding = bounding - self.bounders = None - self.interval = interval - - def fit(self, instances: FeatureData) -> Self: - """Fit the defined model to the supplied instances.""" - if instances.labels is None: - raise ValueError("Labels are required to fit this model.") - self.model_h1.fit(instances.features[instances.labels == 1]) - self.model_h2.fit(instances.features[instances.labels == 0]) - if self.bounding is not None: - # determine the bounds based on the LLRs of the training data, each sample results into an LR-system - logp_h1 = self.model_h1.transform(instances.features) - logp_h2 = self.model_h2.transform(instances.features) - llrs = logp_h1 - logp_h2 - # determine the bounds for each LR-system individually - self.bounders = [self.bounding.__class__() for _ in range(llrs.shape[1])] - for i_system in range(llrs.shape[1]): - self.bounders[i_system] = self.bounders[i_system].fit(llrs[:, i_system], instances.labels) - return self - - def transform(self, instances: FeatureData) -> LLRData: - """Apply the fitted model to the supplied instances.""" - logp_h1 = self.model_h1.transform(instances.features) - logp_h2 = self.model_h2.transform(instances.features) - llrs = logp_h1 - logp_h2 - if (self.bounding is not None) and (self.bounders is not None): - # apply the bounders one by one - for i_system in range(llrs.shape[1]): - llrs[:, i_system] = self.bounders[i_system].transform(llrs[:, i_system]) - quantiles = np.quantile(llrs, [0.5] + list(self.interval), axis=1, method="midpoint") - return instances.replace_as(LLRData, features=quantiles.transpose(1, 0)) - - -class McmcModel: - def __init__( # noqa: PLR0913 (more than five arguments are allowed) - self, - distribution: str, - parameters: dict[str, dict[str, int]] | None, - chain_count: int = 4, - tune_count: int = 1000, - draw_count: int = 1000, - random_seed: int | None = None, - ): - """ - Define the MCMC model and settings to be used. - - :param distribution: statistical distribution used, for example 'normal' or 'binomial' - :param parameters: definition of the parameters of the distribution, and their prior distributions; see below. - :param chain_count: number of parallel mcmc chains - :param tune_count: number of tune/warm-up/burn-in samples per chain - :param draw_count: number of samples to draw from each chain - :param random_seed: random seed - - Currently supported distributions are: 'betabinomial', 'binomial', 'normal'. - Names of the parameters are based on the nomenclature used in pymc for distribution parameters: - https://www.pymc.io/projects/docs/en/stable/api/distributions.html. The parameters should be provided as a - dictionary where the keys are the names of the parameter used for the selected statistical distribution, and the - values are dictionaries with a key 'prior', defining the prior distribution used for that parameter (currently - supported values are 'beta', 'normal' and 'uniform'), and with additional keys corresponding to the names of the - parameters used for that prior distribution (the dict values are the values of these parameters). - For example, for a binomial distribution: parameters = {'p': {'prior': 'beta', 'alpha': 0.5, 'beta': 0.5}}. - Or for a betabinomial distribution: parameters = {'alpha': {'prior': 'uniform', 'lower': 0.01, 'upper': 100}, - 'beta': {'prior': 'uniform', 'lower': 0.01, 'upper': 100}}. - """ - self.distribution = distribution - self.parameters = parameters - self.chain_count = chain_count - self.tune_count = tune_count - self.draw_count = draw_count - self.random_seed = random_seed - self.parameter_samples = {} - self.r_hat = None - - def fit(self, features: np.ndarray) -> Self: - """ - Draw samples from the posterior distributions of the parameters of a specified statistical distribution. - - The posteriors are based on the specified prior distributions of these parameters and observed feature values. - - :param features: observed feature values, used to update the prior distributions of the parameters with - """ - if self.parameters is None: - raise ValueError("Distribution parameters not specified.") - # It looks like all pymc stuff needs to be in a single model block - with pm.Model(): - # Define the prior distributions of the model parameters based on their definitions - priors = {} - for parameter, parameter_input in self.parameters.items(): - if parameter_input["prior"] == "beta": - prior = pm.Beta(parameter, alpha=parameter_input["alpha"], beta=parameter_input["beta"]) - elif parameter_input["prior"] == "normal": - prior = pm.Normal(parameter, mu=parameter_input["mu"], sigma=parameter_input["sigma"]) - elif parameter_input["prior"] == "uniform": - prior = pm.Uniform(parameter, lower=parameter_input["lower"], upper=parameter_input["upper"]) - else: - raise ValueError("Unrecognized prior") - priors.update({parameter: prior}) - # Define the model: priors and the observed data - if self.distribution == "betabinomial": - pm.BetaBinomial( - "k", alpha=priors["alpha"], beta=priors["beta"], n=features[:, 1], observed=features[:, 0] - ) - elif self.distribution == "binomial": - pm.Binomial("k", p=priors["p"], n=np.sum(features[:, 1]), observed=np.sum(features[:, 0])) - elif self.distribution == "normal": - pm.Normal("x", mu=priors["mu"], sigma=priors["sigma"], observed=features[:, 0]) - else: - raise ValueError("Unrecognized distribution") - # Do simulations and sample from the posterior distributions - trace = pm.sample( - draws=self.draw_count, - chains=self.chain_count, - tune=self.tune_count, - cores=1, - random_seed=self.random_seed, - progressbar=False, - ) - # Get the posterior samples of the model parameters and convergence statistics - self.parameter_samples = {} - for parameter in list(self.parameters.keys()): - # Combine the samples from all chains - samples = np.concatenate(np.array(trace.posterior[parameter])) # type: ignore [unresolved-attribute] - self.parameter_samples.update({parameter: samples}) - summary = az.summary(trace, round_to=6) - self.r_hat = summary["r_hat"] - return self - - def transform(self, features: np.ndarray) -> np.ndarray: - """ - Get samples of the posterior distribution of the (log10) probability. - - Use the samples of the posterior distributions of the parameters, in combination with the selected statistical - distribution, to get samples of the posterior distribution of the (log10) probability, evaluated for specified - feature values. - - :param features: feature values for which the probabilities are to be calculated - """ - # Prepare features and parameters for 2d-evaluations (number of samples * number of requested feature values) - sample_count = len(self.parameter_samples[list(self.parameter_samples.keys())[0]]) - features_2d = {} - for feature_id in range(features.shape[1]): - feature_2d = np.tile(np.expand_dims(features[:, feature_id], 1), (1, sample_count)) - features_2d.update({feature_id: feature_2d}) - parameters_2d = {} - for parameter in list(self.parameter_samples.keys()): - parameter_2d = np.tile(np.expand_dims(self.parameter_samples[parameter], 0), (len(features), 1)) - parameters_2d.update({parameter: parameter_2d}) - # Calculate e-base log probabilities at specified feature values - if self.distribution == "betabinomial": - logp = betabinom.logpmf(features_2d[0], features_2d[1], parameters_2d["alpha"], parameters_2d["beta"]) - elif self.distribution == "binomial": - logp = binom.logpmf(features_2d[0], features_2d[1], parameters_2d["p"]) - elif self.distribution == "norm": - logp = norm.logpdf(features_2d[0], parameters_2d["mu"], parameters_2d["sigma"]) - else: - raise ValueError("Unrecognized distribution") - # Return 10-base log probabilities - return logp / np.log(10) diff --git a/pdm.lock b/pdm.lock index 4a7b8d1..fd04750 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:a71832e3523e35aa43b294da50303de5b297345dc561d6d3bfaedad055500d04" +content_hash = "sha256:b37e1b0161b5ffe35c02d5f58b49d6c728a9cdf6cd670794b553f279e0c8a77e" [[metadata.targets]] requires_python = ">=3.13,<3.14" @@ -64,6 +64,17 @@ files = [ {file = "arviz-0.22.0.tar.gz", hash = "sha256:d9df7592f1ce77ce69f7504dba13f8d550204c49c23e54849861dbcb2c640954"}, ] +[[package]] +name = "attrs" +version = "25.4.0" +requires_python = ">=3.9" +summary = "Classes Without Boilerplate" +groups = ["default"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + [[package]] name = "cachetools" version = "6.2.2" @@ -75,6 +86,60 @@ files = [ {file = "cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6"}, ] +[[package]] +name = "cattrs" +version = "26.1.0" +requires_python = ">=3.10" +summary = "Composable complex class support for attrs and dataclasses." +groups = ["default"] +dependencies = [ + "attrs>=25.4.0", + "exceptiongroup>=1.1.1; python_version < \"3.11\"", + "typing-extensions>=4.14.0", +] +files = [ + {file = "cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096"}, + {file = "cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40"}, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +requires_python = ">=3.7" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +requires_python = ">=3.7" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["default"] +files = [ + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + [[package]] name = "cloudpickle" version = "3.1.2" @@ -381,6 +446,17 @@ files = [ {file = "h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69"}, ] +[[package]] +name = "idna" +version = "3.11" +requires_python = ">=3.8" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -403,6 +479,37 @@ files = [ {file = "joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55"}, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +requires_python = ">=3.10" +summary = "An implementation of JSON Schema validation for Python" +groups = ["default"] +dependencies = [ + "attrs>=22.2.0", + "jsonschema-specifications>=2023.03.6", + "referencing>=0.28.4", + "rpds-py>=0.25.0", +] +files = [ + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +requires_python = ">=3.9" +summary = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +groups = ["default"] +dependencies = [ + "referencing>=0.31.0", +] +files = [ + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, +] + [[package]] name = "kdepy" version = "1.1.12" @@ -421,12 +528,6 @@ files = [ {file = "kdepy-1.1.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e62facb46ea6d6eff92a0f8ad054bdf4b5a84ec66444b5cae9d5af1f828b07b7"}, {file = "kdepy-1.1.12-cp313-cp313-win32.whl", hash = "sha256:c5fdc115c212df1a622997a9d8ff9c1bae8179562cc8f2e9710adced96abfd20"}, {file = "kdepy-1.1.12-cp313-cp313-win_amd64.whl", hash = "sha256:f64b740a61f60076630cfcdd1ff011c70d1ce1a7ceab05b14763d95163140e46"}, - {file = "kdepy-1.1.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6b0db948fc79a328974aa1d36a71c959fa1293f1398f9e85f787f970410f1ecf"}, - {file = "kdepy-1.1.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:830532bff65c55d8e8db388c3747af177fef3546ce96e1d9477d17f69c5433ac"}, - {file = "kdepy-1.1.12-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e7526917397e999b28e70308fd75102f963f0686629a3b52fe9ea4a7a01a338"}, - {file = "kdepy-1.1.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ae13a57639420ea212e86fe43c7541f28ea5273ff08165d08af650db93bf6217"}, - {file = "kdepy-1.1.12-cp314-cp314-win32.whl", hash = "sha256:33137c685552cebb14ffabc77308d0df9e456746f8a10ddbf951e156da80cdf7"}, - {file = "kdepy-1.1.12-cp314-cp314-win_amd64.whl", hash = "sha256:86028eb0b7d15e32c81e02b22e5f51dfad2336290dbd9ea1851d7e4de47b2676"}, {file = "kdepy-1.1.12.tar.gz", hash = "sha256:eb3a62abc5a982f5a30ec0a3560e2f9cbd3d19bc4f721b2ebbde06949d52af61"}, ] @@ -493,25 +594,43 @@ files = [ [[package]] name = "lir" -version = "1.2.1.dev30" -requires_python = ">=3.11" -git = "https://github.com/NetherlandsForensicInstitute/lir.git" -ref = "335f8e0dc458b3d969b1a126e492c04cbf8499ab" -revision = "335f8e0dc458b3d969b1a126e492c04cbf8499ab" +version = "1.3.3" +requires_python = "<3.15,>=3.12" summary = "Package for optimising and evaluating Likelihood Ratio (LR) systems." groups = ["default"] dependencies = [ "KDEpy>=1.1.12", "confidence>=0.17.1", + "jsonschema>=4.26.0", "matplotlib>=3.5", - "numpy>=1.22", + "numba>=0.63", + "numpy>2", "optuna>=4.4.0", - "pandas>=2.3.2", "pydantic>=2.12.3", + "pymc>=5.21.0", + "requests-cache>=1.2.1", + "requests>=2.32.5", "scikit-learn>=1.2", "scipy>=1.13.1", "tqdm>=4.67.1", ] +files = [ + {file = "lir-1.3.3-py3-none-any.whl", hash = "sha256:290b3886fa11093ed28a56c8a0e80e12840c3b5f2ad8f697a2589ebd10918b93"}, +] + +[[package]] +name = "llvmlite" +version = "0.46.0" +requires_python = ">=3.10" +summary = "lightweight wrapper around basic LLVM functionality" +groups = ["default"] +files = [ + {file = "llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172"}, + {file = "llvmlite-0.46.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6cc19b051753368a9c9f31dc041299059ee91aceec81bd57b0e385e5d5bf1a54"}, + {file = "llvmlite-0.46.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bca185892908f9ede48c0acd547fe4dc1bafefb8a4967d47db6cf664f9332d12"}, + {file = "llvmlite-0.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:67438fd30e12349ebb054d86a5a1a57fd5e87d264d2451bcfafbbbaa25b82a35"}, + {file = "llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb"}, +] [[package]] name = "logical-unification" @@ -699,6 +818,24 @@ files = [ {file = "multipledispatch-1.0.0.tar.gz", hash = "sha256:5c839915465c68206c3e9c473357908216c28383b425361e5d144594bf85a7e0"}, ] +[[package]] +name = "numba" +version = "0.64.0" +requires_python = ">=3.10" +summary = "compiling Python code using LLVM" +groups = ["default"] +dependencies = [ + "llvmlite<0.47,>=0.46.0dev0", + "numpy<2.5,>=1.22", +] +files = [ + {file = "numba-0.64.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3bab2c872194dcd985f1153b70782ec0fbbe348fffef340264eacd3a76d59fd6"}, + {file = "numba-0.64.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:703a246c60832cad231d2e73c1182f25bf3cc8b699759ec8fe58a2dbc689a70c"}, + {file = "numba-0.64.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2e49a7900ee971d32af7609adc0cfe6aa7477c6f6cccdf6d8138538cf7756f"}, + {file = "numba-0.64.0-cp313-cp313-win_amd64.whl", hash = "sha256:396f43c3f77e78d7ec84cdfc6b04969c78f8f169351b3c4db814b97e7acf4245"}, + {file = "numba-0.64.0.tar.gz", hash = "sha256:95e7300af648baa3308127b1955b52ce6d11889d16e8cfe637b4f85d2fca52b1"}, +] + [[package]] name = "numpy" version = "2.3.4" @@ -875,6 +1012,17 @@ files = [ {file = "pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353"}, ] +[[package]] +name = "platformdirs" +version = "4.9.2" +requires_python = ">=3.10" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["default"] +files = [ + {file = "platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd"}, + {file = "platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291"}, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -888,7 +1036,7 @@ files = [ [[package]] name = "pydantic" -version = "2.12.4" +version = "2.12.5" requires_python = ">=3.9" summary = "Data validation using Python type hints" groups = ["default"] @@ -899,8 +1047,8 @@ dependencies = [ "typing-inspection>=0.4.2", ] files = [ - {file = "pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e"}, - {file = "pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac"}, + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, ] [[package]] @@ -927,34 +1075,6 @@ files = [ {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, ] @@ -1140,6 +1260,58 @@ files = [ {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] +[[package]] +name = "referencing" +version = "0.37.0" +requires_python = ">=3.10" +summary = "JSON Referencing + Python" +groups = ["default"] +dependencies = [ + "attrs>=22.2.0", + "rpds-py>=0.7.0", + "typing-extensions>=4.4.0; python_version < \"3.13\"", +] +files = [ + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, +] + +[[package]] +name = "requests" +version = "2.32.5" +requires_python = ">=3.9" +summary = "Python HTTP for Humans." +groups = ["default"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[[package]] +name = "requests-cache" +version = "1.3.0" +requires_python = ">=3.8" +summary = "A persistent cache for python requests" +groups = ["default"] +dependencies = [ + "attrs>=21.2", + "cattrs>=22.2", + "platformdirs>=2.5", + "requests>=2.22", + "url-normalize>=2.0", + "urllib3>=1.25.5", +] +files = [ + {file = "requests_cache-1.3.0-py3-none-any.whl", hash = "sha256:f09f27bbf100c250886acf13a9db35b53cf2852fddd71977b47c71ea7d90dbba"}, + {file = "requests_cache-1.3.0.tar.gz", hash = "sha256:070e357ccef11a300ccef4294a85de1ab265833c5d9c9538b26cd7ba4085d54a"}, +] + [[package]] name = "rich" version = "14.2.0" @@ -1155,6 +1327,45 @@ files = [ {file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"}, ] +[[package]] +name = "rpds-py" +version = "0.30.0" +requires_python = ">=3.10" +summary = "Python bindings to Rust's persistent data structures (rpds)" +groups = ["default"] +files = [ + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, +] + [[package]] name = "ruff" version = "0.14.5" @@ -1423,6 +1634,31 @@ files = [ {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] +[[package]] +name = "url-normalize" +version = "2.2.1" +requires_python = ">=3.8" +summary = "URL normalization for Python" +groups = ["default"] +dependencies = [ + "idna>=3.3", +] +files = [ + {file = "url_normalize-2.2.1-py3-none-any.whl", hash = "sha256:3deb687587dc91f7b25c9ae5162ffc0f057ae85d22b1e15cf5698311247f567b"}, + {file = "url_normalize-2.2.1.tar.gz", hash = "sha256:74a540a3b6eba1d95bdc610c24f2c0141639f3ba903501e61a52a8730247ff37"}, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +requires_python = ">=3.9" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["default"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + [[package]] name = "xarray" version = "2025.10.1" diff --git a/pyproject.toml b/pyproject.toml index bae0ee1..471ac10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,11 @@ readme = "README.md" license = {text = "Apache Software License 2.0"} lir = {url = "https://github.com/NetherlandsForensicInstitute/lir", rev = "335f8e0dc458b3d969b1a126e492c04cbf8499ab" } -dependencies = ["lir", "pandas>=2.3.3", "arviz>=0.22.0", "pymc>=5.26.1", "pytest-randomly>=4.0.1"] +dependencies = [ + "lir==1.3.3", + "pandas>=2.3.3", + "pytest-randomly>=4.0.1", +] [project.urls] homepage = "https://github.com/NetherlandsForensicInstitute/lr_module_scratch/" diff --git a/tests/conftest.py b/tests/conftest.py index 48ec109..f5510f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ import pytest -from lir.data.datasets.synthesized_normal_binary import SynthesizedNormalBinaryData, SynthesizedNormalDataClass from lir.data.models import FeatureData +from lir.datasets.synthesized_normal_binary import SynthesizedNormalBinaryData, SynthesizedNormalData from lir.lrsystems.lrsystems import LRSystem from lrmodule.data_types import MarkType, ModelSettings, ScoreType @@ -11,10 +11,8 @@ def sample_feature_data() -> FeatureData: """Provide FeatureData collection of synthesized normal binary data.""" data = SynthesizedNormalBinaryData( - data_classes={ - 0: SynthesizedNormalDataClass(mean=-1, std=1, size=100), - 1: SynthesizedNormalDataClass(mean=1, std=1, size=100), - }, + SynthesizedNormalData(mean=1, std=1, size=100), + SynthesizedNormalData(mean=-1, std=1, size=100), seed=0, ) data = data.get_instances() diff --git a/tests/mcmc_matlab_comparison/test_mcmc.py b/tests/mcmc_matlab_comparison/test_mcmc.py index 83fd51e..da63c23 100644 --- a/tests/mcmc_matlab_comparison/test_mcmc.py +++ b/tests/mcmc_matlab_comparison/test_mcmc.py @@ -4,10 +4,9 @@ import numpy as np import pandas as pd import pytest +from lir.algorithms.mcmc import McmcModel, McmcLLRModel from lir.data.models import FeatureData -from lrmodule.mcmc import McmcLLRModel, McmcModel - # Tests against references from the Matlab implementation. base_directory = Path(__file__).parent @@ -96,7 +95,7 @@ def test_llr_dataset(dataset_name: str): random_seed=cfg.random_seed, ) model.fit(FeatureData(features=features, labels=labels)) - llrs = model.transform(FeatureData(features=scores_eval)) + llrs = model.apply(FeatureData(features=scores_eval)) llrs_ref = np.loadtxt(base_directory / (csv_prefix + "-llr_unbound.csv"), delimiter=",") assert np.allclose(llrs.llrs, llrs_ref[1], rtol=5e-2, atol=5e-2) assert np.allclose(llrs.llr_intervals[:, 0], llrs_ref[0], rtol=5e-2, atol=5e-2) # type: ignore diff --git a/tests/test_input_data.py b/tests/test_input_data.py index 3074cab..f1b206e 100644 --- a/tests/test_input_data.py +++ b/tests/test_input_data.py @@ -1,43 +1,48 @@ from pathlib import Path +import numpy as np from lir.data.models import FeatureData -from lrmodule.input_data import ScratchData from numpy import array +from lrmodule.input_data import ScratchCsvReader, PredefinedCrossValidation + def test_input_data_to_instances(): """Check that input data is correctly parsed to instances (having multiple folds).""" # Arrange input_file = Path(__file__).parent / "fixtures/input_data/train_test_data.csv" - parsed_input_data = ScratchData(input_file) + dataset = ScratchCsvReader(input_file).get_instances() + strategy = PredefinedCrossValidation() # The following train/test splits for the given data_subsets are expected - subset_1 = { - "train": FeatureData(labels=array([1, 0]), features=array([[60.1234, 10, 21], [63.1234, 16, 20]])), - "test": FeatureData(labels=array([1, 0]), features=array([[20.1234, 11, 42], [10.1234, 6, 34]])), - } - - subset_2 = { - "train": FeatureData(labels=array([1, 0]), features=array([[20.1234, 11, 42], [10.1234, 6, 34]])), - "test": FeatureData(labels=array([1, 0]), features=array([[60.1234, 10, 21], [63.1234, 16, 20]])), - } - - subset_3 = { - "train": FeatureData( + subset_1 = [ + FeatureData(labels=array([1, 0]), features=array([[60.1234, 10, 21], [63.1234, 16, 20]])), + FeatureData(labels=array([1, 0]), features=array([[20.1234, 11, 42], [10.1234, 6, 34]])), + ] + + subset_2 = [ + FeatureData(labels=array([1, 0]), features=array([[20.1234, 11, 42], [10.1234, 6, 34]])), + FeatureData(labels=array([1, 0]), features=array([[60.1234, 10, 21], [63.1234, 16, 20]])), + ] + + subset_3 = [ + FeatureData( labels=array([1, 1, 0, 0]), features=array([[60.1234, 10, 21], [20.1234, 11, 42], [10.1234, 6, 34], [63.1234, 16, 20]]), ), - "test": FeatureData(labels=array([0]), features=array([[9.1234, 2, 12]])), - } + FeatureData(labels=array([0]), features=array([[9.1234, 2, 12]])), + ] # Act - data_subsets = list(parsed_input_data) + assert dataset.split.shape == (5, 3), "role assignment shape should match the input data" + assert np.all(dataset.split[:, 0] == np.array(['t', 'v', 'v', 'n', 't'])) + + data_subsets = list(strategy.apply(dataset)) # Assert # The fixture contains 3 subsets of data (3-fold cross validation) assert len(data_subsets) == 3 # noqa: PLR2004 (magic number) - assert data_subsets == [ - (subset_1["train"], subset_1["test"]), - (subset_2["train"], subset_2["test"]), - (subset_3["train"], subset_3["test"]), - ] + + for i, ((actual_train, actual_test), (expected_train, expected_test)) in enumerate(zip(data_subsets, [subset_1, subset_2, subset_3])): + assert FeatureData(features=actual_train.features, labels=actual_train.labels) == expected_train + assert FeatureData(features=actual_test.features, labels=actual_test.labels) == expected_test diff --git a/tests/test_validation_yaml.py b/tests/test_validation_yaml.py index 4e17fb5..fce71d2 100644 --- a/tests/test_validation_yaml.py +++ b/tests/test_validation_yaml.py @@ -1,15 +1,16 @@ from pathlib import Path import confidence -from lir.config.experiment_strategies import parse_experiments_setup +from lir.main import initialize_experiments -def test_validation_yaml(): +def test_validation_yaml(tmpdir: Path): """Test if the validation.yaml file can be parsed correctly. Does not test correctness of the content, only that it can be parsed without errors. Running the whole setup will take too long for a unit test. """ validation_file = Path(__file__).parent.parent / "validation.yaml" - setup, _ = parse_experiments_setup(confidence.loadf(validation_file)) + cfg = confidence.Configuration(confidence.loadf(validation_file), {'output_path': tmpdir}) + setup, _ = initialize_experiments(cfg) diff --git a/validation.yaml b/validation.yaml index eb107af..4937a8b 100644 --- a/validation.yaml +++ b/validation.yaml @@ -1,19 +1,19 @@ -output: output/${timestamp}_validation +output_path: output/${timestamp}_validation data_root: data # update this path -aperture_shear_data: - strategy: lrmodule.input_data.ScratchData +aperture_shear_data: &aperture_shear_data + method: lrmodule.input_data.ScratchCsvReader input_file_path: ${data_root}/aperture_shear.csv -firing_pin_impression_data: - strategy: lrmodule.input_data.ScratchData +firing_pin_impression_data: &firing_pin_impression_data + method: lrmodule.input_data.ScratchCsvReader input_file_path: ${data_root}/firing_pin_impression.csv -breech_face_impression_data: - strategy: lrmodule.input_data.ScratchData +breech_face_impression_data: &breech_face_impression_data + method: lrmodule.input_data.ScratchCsvReader input_file_path: ${data_root}/breech_face_impression.csv -aperture_shear_ccf_lrsystem: +aperture_shear_ccf_lrsystem: &aperture_shear_ccf_lrsystem architecture: lrmodule.binary_lrsystem modules: steps: @@ -33,7 +33,7 @@ aperture_shear_ccf_lrsystem: - 0.23 # KM kernel size elub: elub_bounder -firing_pin_impression_accf_lrsystem: +firing_pin_impression_accf_lrsystem: &firing_pin_impression_accf_lrsystem architecture: lrmodule.binary_lrsystem modules: steps: @@ -54,7 +54,7 @@ firing_pin_impression_accf_lrsystem: - 0.14 # KM kernel size elub: elub_bounder -breech_face_impression_accf_lrsystem: +breech_face_impression_accf_lrsystem: &breech_face_impression_accf_lrsystem architecture: lrmodule.binary_lrsystem modules: steps: @@ -75,7 +75,7 @@ breech_face_impression_accf_lrsystem: - 0.11 # KM kernel size elub: elub_bounder -firing_pin_impression_cmc_lrsystem: +firing_pin_impression_cmc_lrsystem: &firing_pin_impression_cmc_lrsystem architecture: lrmodule.binary_lrsystem modules: steps: @@ -106,7 +106,7 @@ firing_pin_impression_cmc_lrsystem: elub: elub_bounder # does this work with the interval? (TODO: ticket) -breech_face_impression_cmc_lrsystem: +breech_face_impression_cmc_lrsystem: &breech_face_impression_cmc_lrsystem architecture: lrmodule.binary_lrsystem modules: steps: @@ -137,56 +137,68 @@ breech_face_impression_cmc_lrsystem: # elub: elub_bounder # does this work with the interval? (TODO: ticket) experiments: - aperture_shear_model: + - name: aperture_shear_model strategy: single_run - lr_system: ${aperture_shear_ccf_lrsystem} - data: ${aperture_shear_data} - aggregation: - metrics: - - cllr - - cllr_min - visualization: + lr_system: *aperture_shear_ccf_lrsystem + data: + provider: *aperture_shear_data + splits: + strategy: lrmodule.input_data.PredefinedCrossValidation + output: + - method: metrics + columns: + - cllr + - cllr_min - pav - ece - llr_interval # - score_histogram (ongetransformeerd) # - score_histogram (normalized (area=1)) - firing_pin_impression_model: + - name: firing_pin_impression_model strategy: single_run - lr_system: ${firing_pin_impression_accf_lrsystem} - data: ${firing_pin_impression_data} - aggregation: - metrics: - - cllr - - cllr_min - visualization: + lr_system: *firing_pin_impression_accf_lrsystem + data: + provider: *firing_pin_impression_data + splits: + strategy: lrmodule.input_data.PredefinedCrossValidation + output: + - method: metrics + columns: + - cllr + - cllr_min - pav - ece - llr_interval - breech_face_impression_model_accf: + - name: breech_face_impression_model_accf strategy: single_run - lr_system: ${breech_face_impression_accf_lrsystem} - data: ${breech_face_impression_data} - aggregation: - metrics: - - cllr - - cllr_min - visualization: + lr_system: *breech_face_impression_accf_lrsystem + data: + provider: *breech_face_impression_data + splits: + strategy: lrmodule.input_data.PredefinedCrossValidation + output: + - method: metrics + columns: + - cllr + - cllr_min - pav - ece - llr_interval - breech_face_impression_model_cmc: + - name: breech_face_impression_model_cmc strategy: single_run - lr_system: ${breech_face_impression_cmc_lrsystem} - data: ${breech_face_impression_data} - aggregation: - metrics: - - cllr - - cllr_min - visualization: + lr_system: *breech_face_impression_cmc_lrsystem + data: + provider: *breech_face_impression_data + splits: + strategy: lrmodule.input_data.PredefinedCrossValidation + output: + - method: metrics + columns: + - cllr + - cllr_min - pav - ece - llr_interval