diff --git a/.gitattributes b/.gitattributes index 16ef5c5f7..d5799bd69 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,2 @@ # GitHub syntax highlighting pixi.lock linguist-language=YAML - diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index d0861e259..f97a2fcfa 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -52,7 +52,7 @@ jobs: if: github.event_name == 'release' && github.event.action == 'published' needs: [build_wheels, build_sdist] runs-on: ubuntu-latest - environment: + environment: name: test_release url: https://test.pypi.org/p/glum permissions: @@ -70,7 +70,7 @@ jobs: if: github.event_name == 'release' && github.event.action == 'published' needs: [build_wheels, build_sdist, upload_testpypi] runs-on: ubuntu-latest - environment: + environment: name: release url: https://pypi.org/p/glum permissions: diff --git a/.gitignore b/.gitignore index dd1d5ae11..b1a053abc 100644 --- a/.gitignore +++ b/.gitignore @@ -150,4 +150,3 @@ pkgs/* # pixi environments .pixi *.egg-info - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 26f0f1753..cd710d995 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,3 +45,22 @@ repos: language: system types: [python] require_serial: true + # pre-commit-hooks + - id: trailing-whitespace-fixer + name: trailing-whitespace-fixer + entry: pixi run -e lint trailing-whitespace-fixer + language: system + types: [text] + exclude: (\.py|README.md)$ + - id: end-of-file-fixer + name: end-of-file-fixer + entry: pixi run -e lint end-of-file-fixer + language: system + types: [text] + exclude: (\.py|changelog.rst)$ + - id: check-merge-conflict + name: check-merge-conflict + entry: pixi run -e lint check-merge-conflict --assume-in-merge + language: system + types: [text] + exclude: \.py$ diff --git a/LICENSE b/LICENSE index 119fba356..3045e8e2f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020-2021 QuantCo Inc, Christian Lorentzen +Copyright 2020-2021 QuantCo Inc, Christian Lorentzen Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..ea4784217 --- /dev/null +++ b/NOTICE @@ -0,0 +1,3 @@ +Modified from code submitted as a PR to sklearn: https://github.com/scikit-learn/scikit-learn/pull/9405 + +Original attribution from: https://github.com/scikit-learn/scikit-learn/pull/9405/filesdiff-38e412190dc50455611b75cfcf2d002713dcf6d537a78b9a22cc6b1c164390d1 diff --git a/build_tools/prepare_macos_wheel.sh b/build_tools/prepare_macos_wheel.sh index beff29211..f7ff1a459 100644 --- a/build_tools/prepare_macos_wheel.sh +++ b/build_tools/prepare_macos_wheel.sh @@ -10,4 +10,3 @@ else fi conda create -n build -c $CONDA_CHANNEL 'llvm-openmp=11' - diff --git a/docs/contributing.rst b/docs/contributing.rst index 53031e1ce..dee75653f 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -1,7 +1,7 @@ Contributing and Development ==================================== -Hello! And thanks for exploring glum more deeply. Please see the issue tracker and pull requests tabs on Github for information about what is currently happening. Feel free to post an issue if you'd like to get involved in development and don't really know where to start -- we can give some advice. +Hello! And thanks for exploring glum more deeply. Please see the issue tracker and pull requests tabs on Github for information about what is currently happening. Feel free to post an issue if you'd like to get involved in development and don't really know where to start -- we can give some advice. We welcome contributions of any kind! @@ -25,7 +25,7 @@ Pull request process Releases -------------------------------------------------- -- We make package releases infrequently, but usually any time a new non-trivial feature is contributed or a bug is fixed. To make a release, just open a PR that updates the change log with the current date. Once that PR is approved and merged, you can create a new release on [GitHub](https://github.com/Quantco/glum/releases/new). Use the version from the change log as tag and copy the change log entry into the release description. +- We make package releases infrequently, but usually any time a new non-trivial feature is contributed or a bug is fixed. To make a release, just open a PR that updates the change log with the current date. Once that PR is approved and merged, you can create a new release on [GitHub](https://github.com/Quantco/glum/releases/new). Use the version from the change log as tag and copy the change log entry into the release description. Install for development -------------------------------------------------- @@ -75,10 +75,10 @@ The test suite is in ``tests/``. A pixi task is available to run the tests: Golden master tests ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -We use golden master testing to preserve correctness. The results of many different GLM models have been saved. After an update, the tests will compare the new output to the saved models. Any significant deviation will result in a test failure. This doesn't strictly mean that the update was wrong. In case of a bug fix, it's possible that the new output will be more accurate than the old output. In that situation, the golden master results can be overwritten as explained below. +We use golden master testing to preserve correctness. The results of many different GLM models have been saved. After an update, the tests will compare the new output to the saved models. Any significant deviation will result in a test failure. This doesn't strictly mean that the update was wrong. In case of a bug fix, it's possible that the new output will be more accurate than the old output. In that situation, the golden master results can be overwritten as explained below. There are two sets of golden master tests, one with artificial data and one directly using the benchmarking problems from :mod:`glum_benchmarks`. For both sets of tests, creating the golden master and the tests definition are located in the same file. Calling the file with pytest will run the tests while calling the file as a python script will generate the golden master result. When creating the golden master results, both scripts accept the ``--overwrite`` command line flag. If set, the existing golden master results will be overwritten. Otherwise, only the new problems will be run. - + Skipping the slow tests ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -102,7 +102,7 @@ Building a conda package To use the package in another project, we distribute it as a conda package. For building the package locally, you can use the following command: -:: +:: conda build conda.recipe @@ -121,7 +121,7 @@ Then, navigate to ``_ to view the documentation. Alternatively, if you install `entr `_, then you can auto-rebuild the documentation any time a file changes with: -:: +:: cd docs ./dev @@ -141,7 +141,7 @@ If you are a newbie to Sphinx, the links below may help get you up to speed on s Where to start looking in the source? ------------------------------------- -The primary user interface of ``glum`` consists of the :class:`GeneralizedLinearRegressor ` and :class:`GeneralizedLinearRegressorCV ` classes via their constructors and the :meth:`fit() ` and :meth:`predict() ` functions. Those are the places to start looking if you plan to change the system in some way. +The primary user interface of ``glum`` consists of the :class:`GeneralizedLinearRegressor ` and :class:`GeneralizedLinearRegressorCV ` classes via their constructors and the :meth:`fit() ` and :meth:`predict() ` functions. Those are the places to start looking if you plan to change the system in some way. What follows is a high-level summary of the source code structure. For more details, please look in the documentation and docstrings of the relevant classes, functions and methods. @@ -149,7 +149,7 @@ What follows is a high-level summary of the source code structure. For more deta * ``_glm_cv.py`` - This is the entrypoint for the cross validated GLM implementation. It depends on a lot of the code in ``_glm.py`` and only modifies the sections necessary for running training many models with different regularization parameters. * ``_solvers.py`` - This contains the bulk of the IRLS and L-BFGS algorithms for training GLMs. * ``_cd_fast.pyx`` - This is a Cython implementation of the coordinate descent algorithm used for fitting L1 penalty GLMs. Note the ``.pyx`` extension indicating that it is a Cython source file. -* ``_distribution.py`` - definitions of the distributions that can be used. Includes Normal, Poisson, Gamma, InverseGaussian, Tweedie, Binomial and GeneralizedHyperbolicSecant distributions. +* ``_distribution.py`` - definitions of the distributions that can be used. Includes Normal, Poisson, Gamma, InverseGaussian, Tweedie, Binomial and GeneralizedHyperbolicSecant distributions. * ``_link.py`` - definitions of the link functions that can be used. Includes identity, log, logit and Tweedie link functions. * ``_functions.pyx`` - This is a Cython implementation of the log likelihoods, gradients and Hessians for several popular distributions. * ``_util.py`` - This contains a few general purpose linear algebra routines that serve several other modules and don't fit well elsewhere. @@ -157,7 +157,7 @@ What follows is a high-level summary of the source code structure. For more deta The GLM benchmark suite ------------------------ -Before deciding to build a library custom built for our purposes, we did an thorough investigation of the various open source GLM implementations available. This resulted in an extensive suite of benchmarks for comparing the correctness, runtime and availability of features for these libraries. +Before deciding to build a library custom built for our purposes, we did an thorough investigation of the various open source GLM implementations available. This resulted in an extensive suite of benchmarks for comparing the correctness, runtime and availability of features for these libraries. The benchmark suite has two command line entrypoints: @@ -167,4 +167,3 @@ The benchmark suite has two command line entrypoints: Both of these CLI tools take a range of arguments that specify the details of the benchmark problems and which libraries to benchmark. For more details on the benchmark suite, see the README in the source at ``src/glum_benchmarks/README.md``. - diff --git a/docs/getting_started/getting_started.md b/docs/getting_started/getting_started.md index 1fa2026c7..6f4fd892f 100644 --- a/docs/getting_started/getting_started.md +++ b/docs/getting_started/getting_started.md @@ -14,7 +14,7 @@ jupyter: --- -# Getting Started: fitting a Lasso model +# Getting Started: fitting a Lasso model The purpose of this tutorial is to show the basics of `glum`. It assumes a working knowledge of python, regularized linear models, and machine learning. The API is very similar to scikit-learn. After all, `glum` is based on a fork of scikit-learn. @@ -62,7 +62,7 @@ X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split( ## GLM basics: fitting and predicting using the normal family -We'll use `glum.GeneralizedLinearRegressor` to predict the house prices using the available predictors. +We'll use `glum.GeneralizedLinearRegressor` to predict the house prices using the available predictors. We set three key parameters: @@ -118,7 +118,7 @@ which we interact with as in the example above. ## Fitting a GLM with cross validation -Now, we fit using automatic cross validation with `glum.GeneralizedLinearRegressorCV`. This mirrors the commonly used `cv.glmnet` function. +Now, we fit using automatic cross validation with `glum.GeneralizedLinearRegressorCV`. This mirrors the commonly used `cv.glmnet` function. Some important parameters: @@ -130,7 +130,7 @@ Some important parameters: 3. If `min_alpha_ratio` is set, create a path where the ratio of `min_alpha / max_alpha = min_alpha_ratio`. 4. If none of the above parameters are set, use a `min_alpha_ratio` - of 1e-6. + of 1e-6. - `l1_ratio`: for `GeneralizedLinearRegressorCV`, if you pass `l1_ratio` as an array, the `fit` method will choose the best value of `l1_ratio` and store it as `self.l1_ratio_`. ```python diff --git a/docs/index.rst b/docs/index.rst index 954c1cad4..16ea1ce1e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,7 +15,7 @@ Welcome to glum's documentation! .. image:: _static/headline_benchmark.png :width: 600 - + We suggest visiting the :doc:`Installation` and :doc:`Getting Started` sections first. .. toctree:: diff --git a/docs/make.bat b/docs/make.bat index 96ed7097e..922152e96 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -32,4 +32,4 @@ goto end %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end -popd \ No newline at end of file +popd diff --git a/docs/motivation.rst b/docs/motivation.rst index 4cab7704d..a31a2f84e 100644 --- a/docs/motivation.rst +++ b/docs/motivation.rst @@ -3,40 +3,40 @@ Yet another GLM package? ``glum`` was inspired by a desire to have a fast, maintainable, Python-first library for fitting GLMs with an extensive feature set. -At the beginning, we thoroughly examined all the existing contenders. The two mostly feature-complete options were glmnet and H2O. In many ways, the R package "glmnet" is the gold standard for regularized glm implementations. However, it is missing several useful features like built-in support for Tweedie and Gamma distributions. It also suffers from `impossible-to-maintain source `_ and thus has frequent bugs and segfaults. Although Python-to-glmnet interfaces exist, none is complete and well maintained. We also looked into the H2O implementation. It’s more feature-complete than glmnet, but there are serious integration issues with Python. As we discovered, there is also substantial room to improve performance beyond the level of glmnet or H2O. +At the beginning, we thoroughly examined all the existing contenders. The two mostly feature-complete options were glmnet and H2O. In many ways, the R package "glmnet" is the gold standard for regularized glm implementations. However, it is missing several useful features like built-in support for Tweedie and Gamma distributions. It also suffers from `impossible-to-maintain source `_ and thus has frequent bugs and segfaults. Although Python-to-glmnet interfaces exist, none is complete and well maintained. We also looked into the H2O implementation. It's more feature-complete than glmnet, but there are serious integration issues with Python. As we discovered, there is also substantial room to improve performance beyond the level of glmnet or H2O. So we decided to improve an existing package. Which one? To be a bit more precise, the question we wanted to answer was "Which library will be the least work to make feature-complete, high performance and correct?" To decide, we began by building a suite of benchmarks to compare the different libraries, and compared the libraries in terms of speed, the number of benchmarks that ran successfully, and code quality. In the end, we went with the code from `an sklearn pull request `_. We called it "sklearn-fork" and actually gave our code that name for quite a while too. sklearn-fork had decent, understandable code, converged in most situations, and included many of the features that we wanted. But it was slow. We figured it would be easier to speed up a functioning library than fix a broken but fast library. So we decided to start improving sklearn-fork. As a side note, a huge thank you to `Christian `_ for producing the baseline code for ``glum``. -Ultimately, improving sklearn-fork seems to have been the right choice. We feel we have achieved our goals and ``glum`` is now :doc:`feature-complete `, :doc:`high-performance ` and correct. However, over time, we uncovered more flaws in the optimizer than expected and, like most projects, building sklearn-fork into a feature-complete, fast, GLM library was a `harder task `_ than `we predicted `_. When we started, sklearn-fork successfully converged for most problems. But, it was slow, taking hundreds or thousands of iteratively reweighted least squares (IRLS) iterations, many more than other similar libraries. Overall, the improvements we’ve made separate into three categories: algorithmic improvements, detailed software optimizations, and new features. +Ultimately, improving sklearn-fork seems to have been the right choice. We feel we have achieved our goals and ``glum`` is now :doc:`feature-complete `, :doc:`high-performance ` and correct. However, over time, we uncovered more flaws in the optimizer than expected and, like most projects, building sklearn-fork into a feature-complete, fast, GLM library was a `harder task `_ than `we predicted `_. When we started, sklearn-fork successfully converged for most problems. But, it was slow, taking hundreds or thousands of iteratively reweighted least squares (IRLS) iterations, many more than other similar libraries. Overall, the improvements we've made separate into three categories: algorithmic improvements, detailed software optimizations, and new features. Algorithmic improvements ------------------------- -At the beginning, the lowest-hanging fruit came from debugging the implementation of IRLS and coordinate descent (CD) because those components were quite buggy and suboptimal. The algorithm we use is from [Yuan2012]_. We started by understanding the paper and relating it back to the code. This led to a circuitous chase around the code base, an effort that paid off when we noticed a hard-coded value in the optimizer was far too high. Fixing this was a one-line change that gave us 2-4X faster convergence! +At the beginning, the lowest-hanging fruit came from debugging the implementation of IRLS and coordinate descent (CD) because those components were quite buggy and suboptimal. The algorithm we use is from [Yuan2012]_. We started by understanding the paper and relating it back to the code. This led to a circuitous chase around the code base, an effort that paid off when we noticed a hard-coded value in the optimizer was far too high. Fixing this was a one-line change that gave us 2-4X faster convergence! -Another large algorithmic improvement to the optimizer came from centering the predictor matrix to have mean zero. Coordinate descent cycles through one feature at a time, which is a strategy that works poorly with non-centered predictors because changing any coefficient changes the mean. In several cases, zero-centering reduced the total number of IRLS iterations by a factor of two, while leaving solutions unchanged. As we discuss below, centering is nontrivial in the case of a sparse matrix because we don’t want to modify the zero entries and destroy the sparsity. This was a major impetus for starting a tabular matrix handling library, `tabmat `_, as an extension of ``glum``. +Another large algorithmic improvement to the optimizer came from centering the predictor matrix to have mean zero. Coordinate descent cycles through one feature at a time, which is a strategy that works poorly with non-centered predictors because changing any coefficient changes the mean. In several cases, zero-centering reduced the total number of IRLS iterations by a factor of two, while leaving solutions unchanged. As we discuss below, centering is nontrivial in the case of a sparse matrix because we don't want to modify the zero entries and destroy the sparsity. This was a major impetus for starting a tabular matrix handling library, `tabmat `_, as an extension of ``glum``. -Much later on, we made major improvements to the quality of the quadratic approximations for binomial, gamma, and Tweedie distributions, where the the original Hessian approximations turned out to be suboptimal. For the first couple months, we took for granted that the quadratic log-likelihood approximations from sklearn-fork were correct. However, after substantial investigation, it turned out that we were using a Fisher information matrix-based approximation to the hessian rather than the true Hessian. This was done in sklearn-fork because the Fisher information matrix (FIM) is guaranteed to be positive definite for any link function or distribution, a necessary condition for guaranteed convergence. However, in cases where the true Hessian is also positive definite, using it will result in much faster convergence. It turned out that switching to using the true Hessian for these special cases (linear, Poisson, gamma, logistic regression and Tweedie regression for 1 < p < 2) gave huge reductions in the number of IRLS iterations. Some gamma regression problems dropped from taking 50-100 iterations to taking just 5-10 iterations. +Much later on, we made major improvements to the quality of the quadratic approximations for binomial, gamma, and Tweedie distributions, where the the original Hessian approximations turned out to be suboptimal. For the first couple months, we took for granted that the quadratic log-likelihood approximations from sklearn-fork were correct. However, after substantial investigation, it turned out that we were using a Fisher information matrix-based approximation to the hessian rather than the true Hessian. This was done in sklearn-fork because the Fisher information matrix (FIM) is guaranteed to be positive definite for any link function or distribution, a necessary condition for guaranteed convergence. However, in cases where the true Hessian is also positive definite, using it will result in much faster convergence. It turned out that switching to using the true Hessian for these special cases (linear, Poisson, gamma, logistic regression and Tweedie regression for 1 < p < 2) gave huge reductions in the number of IRLS iterations. Some gamma regression problems dropped from taking 50-100 iterations to taking just 5-10 iterations. Other important improvements: * Using numerically stable log-likelihood, gradient and hessian formulas for the binomial distribution. In the naive version, we encounter floating point infinities for large parameter values in intermediate calculations. * Exploring the use of an ADMM iterative L1 solver compared to our current CD solver. We ended up sticking with CD. This helped identify some crucial differences between glum and H2O, which uses an ADMM solver. * Active set iteration where we use heuristics to improve performance in L1-regularized problems by predicting, at the beginning of each iteration, which coefficients are likely to remain zero. This effectively reduces the set of predictors and significantly improves performance in severely L1-regularized problems. -* Making sure that we could correctly compare objective functions between libraries. The meaning of the regularization strength varies depending on the constant factors that multiply the log-likelihood. +* Making sure that we could correctly compare objective functions between libraries. The meaning of the regularization strength varies depending on the constant factors that multiply the log-likelihood. Software optimizations ---------------------- Substantial performance improvements came from many places. -* Removing redundant calculations and storing intermediate results to re-use later. The line search step had a particularly large number of such optimization opportunities. -* Cython-izing the coordinate descent implementation based on a version from sklearn’s Lasso implementation. Several optimizations were possible even beyond the sklearn Lasso implementation and we hope to contribute some of these upstream. +* Removing redundant calculations and storing intermediate results to re-use later. The line search step had a particularly large number of such optimization opportunities. +* Cython-izing the coordinate descent implementation based on a version from sklearn's Lasso implementation. Several optimizations were possible even beyond the sklearn Lasso implementation and we hope to contribute some of these upstream. * Hand-optimizing the formulas and Cython-izing the code for common distributions' log likelihood, gradients, and hessians. We did this for normal, Poisson, gamma, Tweedie, binomial distributions. -The largest performance improvements have come from better tabular matrix handling. Initially, we were only handling uniformly dense or sparse matrices and using numpy and scipy.sparse to perform matrix operation. Now, we handle general "split" matrices that can be represented by a combination of dense, sparse, and categorical subcomponents. In addition, we built a ``StandardizedMatrix`` which handles the offsetting and multiplication needed to standardize a matrix to have mean zero and standard deviation one. We store the offsets and multipliers to perform this operation without modifying the underlying matrices. +The largest performance improvements have come from better tabular matrix handling. Initially, we were only handling uniformly dense or sparse matrices and using numpy and scipy.sparse to perform matrix operation. Now, we handle general "split" matrices that can be represented by a combination of dense, sparse, and categorical subcomponents. In addition, we built a ``StandardizedMatrix`` which handles the offsetting and multiplication needed to standardize a matrix to have mean zero and standard deviation one. We store the offsets and multipliers to perform this operation without modifying the underlying matrices. -We took our first step into developing custom matrix classes when we realized that even the pure dense and sparse matrix implementations were suboptimal. The default scipy.sparse matrix-multiply and matrix-vector product implementations are not parallel. Furthermore, many matrix-vector products only involve a small subset of rows or columns. As a result, we now have custom implementations of these operations that are parallelized and allow operating on a restricted set of rows and columns. +We took our first step into developing custom matrix classes when we realized that even the pure dense and sparse matrix implementations were suboptimal. The default scipy.sparse matrix-multiply and matrix-vector product implementations are not parallel. Furthermore, many matrix-vector products only involve a small subset of rows or columns. As a result, we now have custom implementations of these operations that are parallelized and allow operating on a restricted set of rows and columns. Before continuing, a quick summary of the only three matrix operations that we care about for GLM estimation: @@ -44,24 +44,24 @@ Before continuing, a quick summary of the only three matrix operations that we c * Transpose-matrix-vector products. ``X.T.dot(v)`` * Sandwich products. ``X.T @ diag(d) @ X`` -As a matrix multiplication, the sandwich products are higher-dimensional operations than the matrix-vector products and, as such, are particularly expensive. Not only that, but the default implementation in numpy or scipy.sparse is going to be very inefficient. With dense numpy arrays, if we perform ``X.T @ diag(d)``, that will allocate and create a whole new matrix that’s just as large as the original ``X`` matrix. Then, we still need to perform a matrix multiply! As a result, we implemented a parallelized, cache-friendly, SIMD-optimized sandwich product operation that avoids the copy and performs the operation as a single matrix-multiply-like operation. We are in the process of contributing an implementation to the `BLIS library `_. +As a matrix multiplication, the sandwich products are higher-dimensional operations than the matrix-vector products and, as such, are particularly expensive. Not only that, but the default implementation in numpy or scipy.sparse is going to be very inefficient. With dense numpy arrays, if we perform ``X.T @ diag(d)``, that will allocate and create a whole new matrix that's just as large as the original ``X`` matrix. Then, we still need to perform a matrix multiply! As a result, we implemented a parallelized, cache-friendly, SIMD-optimized sandwich product operation that avoids the copy and performs the operation as a single matrix-multiply-like operation. We are in the process of contributing an implementation to the `BLIS library `_. The next big matrix optimization came from realizing that most data matrices are neither fully dense nor fully sparse. Some columns will be very sparse (e.g. number of parrots owned), some columns will be one-hot encoded categoricals (e.g. preferred parrot species) while other columns will be dense (e.g. volume in liters of the most recently seen parrot). So we built a SplitMatrix class that splits a matrix into dense and sparse subcomponents. A threshold of around 90% sparsity seems to be about the level at which it is beneficial to use a simple CSR sparse matrix instead of a dense matrix. The benefit of this split matrix was large, improving performance across all the matrix operations by 2-5x. Later on, we also added categorical matrix handling to the mix. Many categorical columns will be very sparse. If there are 100 evenly distributed categories, each column will have 99% sparse. However, simply treating them as a general sparse matrix is leaving a lot on the table. Beyond just being sparse, we know that every non-zero entry is a one and that every row has only a single non-zero column. This is particularly beneficial for sandwich products where the output ends up being diagonal. But, despite the clear gains, adding categorical matrices was quite a large undertaking. We needed to modify our data generation process to produce categoricals instead of one-hot-encoded columns, add and optimize each of our matrix operations for categoricals, and specify "sandwich" interactions between categorical matrices, sparse matrices, and dense matrices. The result was a large improvement in runtime, with some sandwich and matrix-transpose-dot operations sped up by more than an order of magnitude. -The end result of all these matrix optimizations is that we now have a fairly complete library for handling simple sandwich, dot and transpose-dot operations on a mix of dense, sparse and categorical matrices. This is perfect for most tabular data! So, we’ve split this component off into its own library, `tabmat `_. +The end result of all these matrix optimizations is that we now have a fairly complete library for handling simple sandwich, dot and transpose-dot operations on a mix of dense, sparse and categorical matrices. This is perfect for most tabular data! So, we've split this component off into its own library, `tabmat `_. New Features ------------- -In addition to the heavy focus on optimization and algorithmic correctness, we’ve also added a few important features to glum beyond what was already available in sklearn-fork. +In addition to the heavy focus on optimization and algorithmic correctness, we've also added a few important features to glum beyond what was already available in sklearn-fork. * Automatic cross validation and regularization path handling similar in behavior to glmnet. -* Linear inequality constraints on coefficients. +* Linear inequality constraints on coefficients. * A step size convergence criterion in addition to the typical gradient-norm based criterion. * The binomial distribution, and as a result, L1 and L2-regularized logistic regression. -* Standard errors. +* Standard errors. References ---------- diff --git a/docs/tutorials/cox_model/cox_model.ipynb b/docs/tutorials/cox_model/cox_model.ipynb index cb9c1cc22..bb7b50ab7 100644 --- a/docs/tutorials/cox_model/cox_model.ipynb +++ b/docs/tutorials/cox_model/cox_model.ipynb @@ -868,7 +868,7 @@ "\n", "[1] Carstensen, B. 2023. Who needs the Cox model anyway. December. Available at: http://bendixcarstensen.com/WntCma.pdf.\n", "\n", - "[2] Whitehead, J., 1980. Fitting Cox’s regression model to survival data using GLIM. _Journal of the Royal Statistical Society Series C: Applied Statistics_, 29(3), pp.268-275." + "[2] Whitehead, J., 1980. Fitting Cox's regression model to survival data using GLIM. _Journal of the Royal Statistical Society Series C: Applied Statistics_, 29(3), pp.268-275." ] } ], diff --git a/docs/tutorials/glm_french_motor_tutorial/glm_french_motor.ipynb b/docs/tutorials/glm_french_motor_tutorial/glm_french_motor.ipynb index 7633e79dc..9164b2c3d 100644 --- a/docs/tutorials/glm_french_motor_tutorial/glm_french_motor.ipynb +++ b/docs/tutorials/glm_french_motor_tutorial/glm_french_motor.ipynb @@ -469,7 +469,7 @@ "\n", "See the `GeneralizedLinearRegressor` class [API documentation](https://glum.readthedocs.io/en/latest/api/modules.html) for more details.\n", "\n", - "*Note*: `glum` also supported a cross validation model GeneralizedLinearRegressorCV. However, because cross validation requires fitting many models, it is much slower and we don’t demonstrate it in this tutorial." + "*Note*: `glum` also supported a cross validation model GeneralizedLinearRegressorCV. However, because cross validation requires fitting many models, it is much slower and we don't demonstrate it in this tutorial." ] }, { diff --git a/docs/tutorials/glm_french_motor_tutorial/glm_french_motor.md b/docs/tutorials/glm_french_motor_tutorial/glm_french_motor.md index 06d01fab2..2ceaf92eb 100644 --- a/docs/tutorials/glm_french_motor_tutorial/glm_french_motor.md +++ b/docs/tutorials/glm_french_motor_tutorial/glm_french_motor.md @@ -161,7 +161,7 @@ Now, we define our GLM using the `GeneralizedLinearRegressor` class from `glum`. See the `GeneralizedLinearRegressor` class [API documentation](https://glum.readthedocs.io/en/latest/api/modules.html) for more details. -*Note*: `glum` also supported a cross validation model GeneralizedLinearRegressorCV. However, because cross validation requires fitting many models, it is much slower and we don’t demonstrate it in this tutorial. +*Note*: `glum` also supported a cross validation model GeneralizedLinearRegressorCV. However, because cross validation requires fitting many models, it is much slower and we don't demonstrate it in this tutorial. ```python f_glm1 = GeneralizedLinearRegressor(family='poisson', alpha_search=True, l1_ratio=1, fit_intercept=True) diff --git a/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/LICENCE.txt b/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/LICENCE.txt index 57e29fa13..368bb088e 100644 --- a/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/LICENCE.txt +++ b/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/LICENCE.txt @@ -21,4 +21,4 @@ Le résumé explicatif n'est pas un contrat, mais simplement une source pratique Le texte complet du contrat de licence (en anglais) est disponible sur: http://opendatacommons.org/licenses/odbl/1.0/ -Une traduction en français est disponible sur http://www.vvlibri.org/fr/licence/odbl/10/fr/legalcode \ No newline at end of file +Une traduction en français est disponible sur http://www.vvlibri.org/fr/licence/odbl/10/fr/legalcode diff --git a/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-20140306-100m.cpg b/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-20140306-100m.cpg index 3ad133c04..7edc66b06 100644 --- a/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-20140306-100m.cpg +++ b/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-20140306-100m.cpg @@ -1 +1 @@ -UTF-8 \ No newline at end of file +UTF-8 diff --git a/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-20140306-100m.prj b/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-20140306-100m.prj index a30c00a55..8f73f480f 100644 --- a/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-20140306-100m.prj +++ b/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-20140306-100m.prj @@ -1 +1 @@ -GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] \ No newline at end of file +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] diff --git a/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-descriptif.txt b/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-descriptif.txt index 24a0fbe56..8c8f3e6cf 100644 --- a/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-descriptif.txt +++ b/docs/tutorials/glm_french_motor_tutorial/regions-20140306-100m-shp/regions-descriptif.txt @@ -19,7 +19,7 @@ Ces fichiers sont proposés au format shapefile, en projection WGS84 avec plusie La topologie est conservée lors du processus de simplification (cf: http://openstreetmap.fr/blogs/cquest/limites-administratives-simplifiees) -= Contenu = += Contenu = Ces fichiers contiennent l'ensemble des régions françaises, y compris les DOM et Mayotte. diff --git a/docs/tutorials/regularization_housing_data/Zip_Codes/Zip_Codes.cpg b/docs/tutorials/regularization_housing_data/Zip_Codes/Zip_Codes.cpg index 3ad133c04..7edc66b06 100644 --- a/docs/tutorials/regularization_housing_data/Zip_Codes/Zip_Codes.cpg +++ b/docs/tutorials/regularization_housing_data/Zip_Codes/Zip_Codes.cpg @@ -1 +1 @@ -UTF-8 \ No newline at end of file +UTF-8 diff --git a/docs/tutorials/regularization_housing_data/Zip_Codes/Zip_Codes.prj b/docs/tutorials/regularization_housing_data/Zip_Codes/Zip_Codes.prj index f45cbadf0..0ae685b47 100644 --- a/docs/tutorials/regularization_housing_data/Zip_Codes/Zip_Codes.prj +++ b/docs/tutorials/regularization_housing_data/Zip_Codes/Zip_Codes.prj @@ -1 +1 @@ -GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]] \ No newline at end of file +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]] diff --git a/docs/tutorials/regularization_housing_data/regularization_housing.md b/docs/tutorials/regularization_housing_data/regularization_housing.md index cf17e6049..f02f69a42 100644 --- a/docs/tutorials/regularization_housing_data/regularization_housing.md +++ b/docs/tutorials/regularization_housing_data/regularization_housing.md @@ -20,12 +20,12 @@ jupyter: This tutorial shows how to use variable $L_2$ regularization with glum. The `P2` parameter of the `GeneralizedLinearRegressor` class allows you to directly set the $L_2$ penalty matrix $w^T P_2 w$. If a 2d array is passed for the `P2` parameter, it is used directly, while if you pass a 1d array as `P2` it will be interpreted as the diagonal of $P_2$ and all other entries will be assumed to be zero. -*Note*: Variable $L_1$ regularization is also available by passing an array with length `n_features` to the `P1` parameter. +*Note*: Variable $L_1$ regularization is also available by passing an array with length `n_features` to the `P1` parameter. **Background** -For this tutorial, we will model the selling price of homes in King's County, Washington (Seattle-Tacoma Metro area) between May 2014 and May 2015. However, in order to demonstrate a Tikhonov regularization-based spatial smoothing technique, we will focus on a small, skewed data sample from that region in our training data. Specifically, we will show that when we have (a) a fixed effect for each postal code region and (b) only a select number of training observations in a certain region, we can improve the predictive power of our model by regularizing the difference between the coefficients of neighboring regions. While we are constructing a somewhat artificial example here in order to demonstrate the spatial smoothing technique, we have found similar techniques to be applicable to real-world problems. +For this tutorial, we will model the selling price of homes in King's County, Washington (Seattle-Tacoma Metro area) between May 2014 and May 2015. However, in order to demonstrate a Tikhonov regularization-based spatial smoothing technique, we will focus on a small, skewed data sample from that region in our training data. Specifically, we will show that when we have (a) a fixed effect for each postal code region and (b) only a select number of training observations in a certain region, we can improve the predictive power of our model by regularizing the difference between the coefficients of neighboring regions. While we are constructing a somewhat artificial example here in order to demonstrate the spatial smoothing technique, we have found similar techniques to be applicable to real-world problems. We will use a gamma distribution for our model. This choice is motivated by two main factors. First, our target variable, home price, is a positive real number, which matches the support of the gamma distribution. Second, it is expected that factors influencing housing prices are multiplicative rather than additive, which is better captured with a gamma regression than say, OLS. @@ -72,11 +72,11 @@ import maps ### 1.1. Download and transform -The main dataset is downloaded from OpenML. You can find the main page for the dataset [here](https://www.openml.org/d/42092). It is also available through Kaggle [here](https://www.kaggle.com/harlfoxem/housesalesprediction). +The main dataset is downloaded from OpenML. You can find the main page for the dataset [here](https://www.openml.org/d/42092). It is also available through Kaggle [here](https://www.kaggle.com/harlfoxem/housesalesprediction). As part of data preparation, we also do some transformations to the data: -- We remove some outliers (homes over 1.5 million and under 100k). +- We remove some outliers (homes over 1.5 million and under 100k). - Since we want to focus on geographic features, we also remove a handful of the other features. Below, you can see some example rows from the dataset. @@ -89,7 +89,7 @@ df.head() ## 2. Visualize geographic data with GIS open [back to table of contents](#Table-of-Contents) -To help visualize the geographic data, we use geopandas and GIS Open Data to display price information on the King's county map. You can get the map data [here]("https://gis-kingcounty.opendata.arcgis.com/datasets/all-zipcodes-and-po-box-as-centroids-for-king-county-zipcode-all-point/data?geometry=-126.017%2C46.845%2C-116.788%2C48.144&page=2"). +To help visualize the geographic data, we use geopandas and GIS Open Data to display price information on the King's county map. You can get the map data [here]("https://gis-kingcounty.opendata.arcgis.com/datasets/all-zipcodes-and-po-box-as-centroids-for-king-county-zipcode-all-point/data?geometry=-126.017%2C46.845%2C-116.788%2C48.144&page=2"). To show the relatioship between home price and geography, we merge the map data with our sales data and use a heat map to plot mean home sale price for each postal code region. @@ -116,9 +116,9 @@ We can see a clear relationship between postal code and home price. Seattle (981 ### 3.1 Feature selection and one hot encoding -Since we want to focus on geographic data, we drop a number of columns below. We keep a handful of columns so that we can still create a reasonable model. +Since we want to focus on geographic data, we drop a number of columns below. We keep a handful of columns so that we can still create a reasonable model. -We then create a fixed effect for each of the postal code regions. We add the encoded postcode columns in numeric order to help us maintain the proper order of columns while building and training the model. +We then create a fixed effect for each of the postal code regions. We add the encoded postcode columns in numeric order to help us maintain the proper order of columns while building and training the model. ```python sorted_zips = sorted(list(df["zipcode"].unique())) @@ -132,7 +132,7 @@ df.head() ### 3.2 Test train split As we mentioned in the introduction, we want to focus on modeling the selling price in a specific region while only using a very small, skewed data sample from that region in our training data. This scenario could arise if say, our task was to predict the sales prices for homes in Enumclaw (large region with zip code 98022 in the southeast corner of the map), but the only data we had from there was from a small luxury realtor. -To mimic this, instead creating a random split between our training and test data, we will intentionally create a highly skewed sample. For our test set, we will take all of the home sales in Enumclaw, *except* for the 15 highest priced homes. +To mimic this, instead creating a random split between our training and test data, we will intentionally create a highly skewed sample. For our test set, we will take all of the home sales in Enumclaw, *except* for the 15 highest priced homes. Finally, we standardize our predictors. @@ -165,7 +165,7 @@ To smooth the coefficients for neighboring regions, we will create a penalty mat $$\begin{pmatrix} \beta_{98022}, \beta_{98045}\end{pmatrix} P \begin{pmatrix} \beta_{98022} \\ \beta_{98045}\end{pmatrix} = (\beta_{98022} - \beta_{98045})^2$$ -In this example, we would get this result with $P = \begin{pmatrix} 1 & -1 \\ -1 & 1\end{pmatrix}$. +In this example, we would get this result with $P = \begin{pmatrix} 1 & -1 \\ -1 & 1\end{pmatrix}$. Since we have 72 postal code regions, it would be rather annoying to construct this matrix by hand. Luckily, there are libraries that exist for this. We use [pysal](http://pysal.org)'s [pysal.lib.weights.Queen](https://pysal.org/libpysal/generated/libpysal.weights.Queen.html) to retrieve a neighbor's matrix from our map data. The construction of the penalty matrix is rather straightforward once we have this information. @@ -173,7 +173,7 @@ We leave the non-geographic features unregularized (all zeros in the $P$ matrix) ```python # format is {zip1: {neighbord1: 1, neighbor2: 1, ...}} -neighbor_matrix = libpysal.weights.Queen.from_dataframe(df_map, ids="ZIP") +neighbor_matrix = libpysal.weights.Queen.from_dataframe(df_map, ids="ZIP") n_features = X_train.shape[1] P2 = np.zeros((n_features, n_features)) @@ -181,7 +181,7 @@ P2 = np.zeros((n_features, n_features)) zip2index = dict(zip(sorted_zips, range(len(sorted_zips)))) for zip1 in sorted_zips: for zip2 in neighbor_matrix[zip1].keys(): - if zip1 in zip2index and zip2 in zip2index: # ignore regions w/o data + if zip1 in zip2index and zip2 in zip2index: # ignore regions w/o data if zip2index[zip1] < zip2index[zip2]: # don't repeat if already saw neighbor pair in earlier iteration P2[zip2index[zip1], zip2index[zip1]] += 1 P2[zip2index[zip2], zip2index[zip2]] += 1 @@ -193,33 +193,33 @@ P2 ## 5. Fit models [back to table of contents](#Table-of-Contents) -Now, we will fit several L2 regularized OLS models using different levels of regularization. All will use the penalty matrix defined above, but the alpha parameter, the constant that multiplies the penalty terms and thus determines the regularization strength, will vary. +Now, we will fit several L2 regularized OLS models using different levels of regularization. All will use the penalty matrix defined above, but the alpha parameter, the constant that multiplies the penalty terms and thus determines the regularization strength, will vary. For each model, we will measure test performance using root mean squared percentage error (RMSPE), so that we can get a relaitve result. We will also plot a heatmat of the coefficient values over the regions. -*Note*: alpha=1e-12 is effectively no regularization. But we can't set alpha to zero because the unregularized problem has co-linear columns, resulting in a singular design matrix. +*Note*: alpha=1e-12 is effectively no regularization. But we can't set alpha to zero because the unregularized problem has co-linear columns, resulting in a singular design matrix. ```python fig, axs = plt.subplots(nrows=2, ncols=2, figsize=(20, 20)) for i, alpha in enumerate([1e-12, 1e-1, 1, 10]): - + glm = GeneralizedLinearRegressor(family='gamma', alpha=alpha, P2=P2, fit_intercept=True) glm.fit(X_train, y_train) y_test_hat = glm.predict(X_test) - + coeffs = pd.DataFrame({'coefficient': np.concatenate(([glm.intercept_], glm.coef_))}, ["intercept"]+predictors) - + print(f"alpha={alpha}") print(f"Test region coefficient: {coeffs.loc[test_region].values[0]}") print(f"Test RMSPE: {root_mean_squared_percentage_error(y_test_hat, y_test)}\n") - + df_map_coeffs = df_map.merge( coeffs.loc[sorted_zips], left_on="ZIP", right_index=True, how="outer" ) - + ax = axs[i//2, i%2] df_map_coeffs["annotation"] = df_map_coeffs["ZIP"].apply(lambda x: "" if x!=test_region else x) maps.plot_heatmap( @@ -231,7 +231,7 @@ for i, alpha in enumerate([1e-12, 1e-1, 1, 10]): vmax=0.025 ) ax.set_title(f"alpha={alpha}") - + plt.show() ``` diff --git a/docs/tutorials/rossman/.gitignore b/docs/tutorials/rossman/.gitignore index fc0253e20..c7ff6b71f 100644 --- a/docs/tutorials/rossman/.gitignore +++ b/docs/tutorials/rossman/.gitignore @@ -1,3 +1,3 @@ *.csv *.parquet -*.json \ No newline at end of file +*.json diff --git a/pixi.lock b/pixi.lock index 18fba2146..6bd163b44 100644 --- a/pixi.lock +++ b/pixi.lock @@ -2621,12 +2621,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.0-h7b32b05_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.0.1-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-hooks-5.0.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.12.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-5_cp312.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h66e93f0_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.10-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.8-py312h66e93f0_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.8.6-py312h2156523_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.6.0-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda @@ -2658,12 +2661,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.0-h81ee809_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.0.1-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-hooks-5.0.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.12.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.1-h4f43103_102_cp313.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.13-5_cp313.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py313h20a7fcf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml-0.18.10-py313h90d716c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml.clib-0.2.8-py313h63a2874_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.8.6-py313heab95af_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.6.0-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda @@ -2693,11 +2699,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.0-ha4e3fda_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.0.1-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-hooks-5.0.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.12.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.1-h071d269_102_cp313.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py313ha7868ed_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml-0.18.10-py313ha7868ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml.clib-0.2.8-py313ha7868ed_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.8.6-py313h331c231_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.6.0-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda @@ -5604,15 +5613,11 @@ packages: - conda: https://conda.anaconda.org/conda-forge/win-64/_libavif_api-1.1.1-h57928b3_2.conda sha256: b99b8948a170ff721ea958ee04a4431797070e85dd6942cb27b73ac3102e5145 md5: 76cf1f62c9a62d6b8f44339483e0f016 - arch: x86_64 - platform: win size: 9286 timestamp: 1730268773319 - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 md5: d7c89558ba9fa0495403155b64376d81 - arch: x86_64 - platform: linux license: None size: 2562 timestamp: 1578324546067 @@ -5625,8 +5630,6 @@ packages: - libgomp >=7.5.0 constrains: - openmp_impl 9999 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 23621 @@ -5638,8 +5641,6 @@ packages: depends: - _libgcc_mutex 0.1 conda_forge - llvm-openmp >=9.0.1 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 5744 @@ -5654,8 +5655,6 @@ packages: constrains: - openmp_impl 9999 - msys2-conda-epoch <0.0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 49468 @@ -5675,8 +5674,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: LGPL-2.1-or-later license_family: GPL size: 560238 @@ -5718,8 +5715,6 @@ packages: depends: - libgcc-ng >=12 - libstdcxx-ng >=12 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 2706396 @@ -5730,8 +5725,6 @@ packages: depends: - __osx >=11.0 - libcxx >=16 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 2235747 @@ -5743,8 +5736,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 1958151 @@ -5780,8 +5771,6 @@ packages: - libgcc >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 34847 @@ -5795,8 +5784,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 32838 @@ -5811,8 +5798,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 34399 @@ -5900,8 +5885,6 @@ packages: - aws-c-io >=0.15.3,<0.15.4.0a0 - aws-c-sdkutils >=0.2.1,<0.2.2.0a0 - libgcc >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 107614 @@ -5916,8 +5899,6 @@ packages: - aws-c-http >=0.9.2,<0.9.3.0a0 - aws-c-io >=0.15.3,<0.15.4.0a0 - aws-c-sdkutils >=0.2.1,<0.2.2.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 92507 @@ -5934,8 +5915,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 102644 @@ -5948,8 +5927,6 @@ packages: - aws-c-common >=0.10.6,<0.10.7.0a0 - libgcc >=13 - openssl >=3.3.1,<4.0a0 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 47601 @@ -5961,8 +5938,6 @@ packages: - __osx >=11.0 - aws-c-common >=0.10.6,<0.10.7.0a0 - openssl >=3.3.1,<4.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 39925 @@ -5976,8 +5951,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 47436 @@ -5988,8 +5961,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 236574 @@ -5999,8 +5970,6 @@ packages: md5: 145e5b4c9702ed279d7d68aaf096f77d depends: - __osx >=11.0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 221863 @@ -6012,8 +5981,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 235514 @@ -6025,8 +5992,6 @@ packages: - __glibc >=2.17,<3.0.a0 - aws-c-common >=0.10.6,<0.10.7.0a0 - libgcc >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 19086 @@ -6037,8 +6002,6 @@ packages: depends: - __osx >=11.0 - aws-c-common >=0.10.6,<0.10.7.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 18068 @@ -6051,8 +6014,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 22364 @@ -6067,8 +6028,6 @@ packages: - aws-checksums >=0.2.2,<0.2.3.0a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 54003 @@ -6082,8 +6041,6 @@ packages: - aws-c-io >=0.15.3,<0.15.4.0a0 - aws-checksums >=0.2.2,<0.2.3.0a0 - libcxx >=18 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 47078 @@ -6098,8 +6055,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 54426 @@ -6114,8 +6069,6 @@ packages: - aws-c-compression >=0.3.0,<0.3.1.0a0 - aws-c-io >=0.15.3,<0.15.4.0a0 - libgcc >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 197731 @@ -6129,8 +6082,6 @@ packages: - aws-c-common >=0.10.6,<0.10.7.0a0 - aws-c-compression >=0.3.0,<0.3.1.0a0 - aws-c-io >=0.15.3,<0.15.4.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 152983 @@ -6146,8 +6097,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 182269 @@ -6161,8 +6110,6 @@ packages: - aws-c-common >=0.10.6,<0.10.7.0a0 - libgcc >=13 - s2n >=1.5.10,<1.5.11.0a0 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 157864 @@ -6174,8 +6121,6 @@ packages: - __osx >=11.0 - aws-c-cal >=0.8.1,<0.8.2.0a0 - aws-c-common >=0.10.6,<0.10.7.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 135729 @@ -6189,8 +6134,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 159852 @@ -6204,8 +6147,6 @@ packages: - aws-c-http >=0.9.2,<0.9.3.0a0 - aws-c-io >=0.15.3,<0.15.4.0a0 - libgcc >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 194672 @@ -6218,8 +6159,6 @@ packages: - aws-c-common >=0.10.6,<0.10.7.0a0 - aws-c-http >=0.9.2,<0.9.3.0a0 - aws-c-io >=0.15.3,<0.15.4.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 134371 @@ -6234,8 +6173,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 186987 @@ -6253,8 +6190,6 @@ packages: - aws-checksums >=0.2.2,<0.2.3.0a0 - libgcc >=13 - openssl >=3.4.0,<4.0a0 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 114156 @@ -6270,8 +6205,6 @@ packages: - aws-c-http >=0.9.2,<0.9.3.0a0 - aws-c-io >=0.15.3,<0.15.4.0a0 - aws-checksums >=0.2.2,<0.2.3.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 97598 @@ -6289,8 +6222,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 109362 @@ -6302,8 +6233,6 @@ packages: - __glibc >=2.17,<3.0.a0 - aws-c-common >=0.10.6,<0.10.7.0a0 - libgcc >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 56094 @@ -6314,8 +6243,6 @@ packages: depends: - __osx >=11.0 - aws-c-common >=0.10.6,<0.10.7.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 49664 @@ -6328,8 +6255,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 55419 @@ -6341,8 +6266,6 @@ packages: - __glibc >=2.17,<3.0.a0 - aws-c-common >=0.10.6,<0.10.7.0a0 - libgcc >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 72762 @@ -6353,8 +6276,6 @@ packages: depends: - __osx >=11.0 - aws-c-common >=0.10.6,<0.10.7.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 70186 @@ -6367,8 +6288,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 91909 @@ -6389,8 +6308,6 @@ packages: - aws-c-sdkutils >=0.2.1,<0.2.2.0a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 354703 @@ -6410,8 +6327,6 @@ packages: - aws-c-s3 >=0.7.7,<0.7.8.0a0 - aws-c-sdkutils >=0.2.1,<0.2.2.0a0 - libcxx >=18 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 236249 @@ -6432,8 +6347,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 262833 @@ -6452,8 +6365,6 @@ packages: - libstdcxx >=13 - libzlib >=1.3.1,<2.0a0 - openssl >=3.4.0,<4.0a0 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 3060561 @@ -6471,8 +6382,6 @@ packages: - libcxx >=18 - libzlib >=1.3.1,<2.0a0 - openssl >=3.4.0,<4.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 2826534 @@ -6489,8 +6398,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 2969711 @@ -6504,8 +6411,6 @@ packages: - libgcc >=13 - libstdcxx >=13 - openssl >=3.3.2,<4.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 345117 @@ -6518,8 +6423,6 @@ packages: - libcurl >=8.10.1,<9.0a0 - libcxx >=17 - openssl >=3.3.2,<4.0a0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 294299 @@ -6533,8 +6436,6 @@ packages: - libgcc >=13 - libstdcxx >=13 - openssl >=3.3.2,<4.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 232351 @@ -6547,8 +6448,6 @@ packages: - azure-core-cpp >=1.14.0,<1.14.1.0a0 - libcxx >=17 - openssl >=3.3.2,<4.0a0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 166907 @@ -6562,8 +6461,6 @@ packages: - azure-storage-common-cpp >=12.8.0,<12.8.1.0a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 549342 @@ -6576,8 +6473,6 @@ packages: - azure-core-cpp >=1.14.0,<1.14.1.0a0 - azure-storage-common-cpp >=12.8.0,<12.8.1.0a0 - libcxx >=17 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 438636 @@ -6592,8 +6487,6 @@ packages: - libstdcxx >=13 - libxml2 >=2.12.7,<3.0a0 - openssl >=3.3.2,<4.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 149312 @@ -6607,8 +6500,6 @@ packages: - libcxx >=17 - libxml2 >=2.12.7,<3.0a0 - openssl >=3.3.2,<4.0a0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 121278 @@ -6623,8 +6514,6 @@ packages: - azure-storage-common-cpp >=12.8.0,<12.8.1.0a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 287366 @@ -6638,8 +6527,6 @@ packages: - azure-storage-blobs-cpp >=12.13.0,<12.13.1.0a0 - azure-storage-common-cpp >=12.8.0,<12.8.1.0a0 - libcxx >=17 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 196032 @@ -6669,8 +6556,6 @@ packages: md5: 348619f90eee04901f4a70615efff35b depends: - binutils_impl_linux-64 >=2.43,<2.44.0a0 - arch: x86_64 - platform: linux license: GPL-3.0-only license_family: GPL size: 33876 @@ -6681,8 +6566,6 @@ packages: depends: - ld_impl_linux-64 2.43 h712a8e2_2 - sysroot_linux-64 - arch: x86_64 - platform: linux license: GPL-3.0-only license_family: GPL size: 5682777 @@ -6692,8 +6575,6 @@ packages: md5: 18aba879ddf1f8f28145ca6fcb873d8c depends: - binutils_impl_linux-64 2.43 h4bf12b8_2 - arch: x86_64 - platform: linux license: GPL-3.0-only license_family: GPL size: 34945 @@ -6715,8 +6596,6 @@ packages: - liblapack 3.9.0 26_linux64_mkl - liblapacke 3.9.0 26_linux64_mkl - llvm-openmp >=19.1.5 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 16451 @@ -6735,8 +6614,6 @@ packages: - vc >=14.3 - vc >=14.3,<15 - vc14_runtime >=14.42.34433 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 18613 @@ -6752,8 +6629,6 @@ packages: - liblapacke 3.9.0 26_linux64_mkl - mkl >=2024.2.2,<2025.0a0 - mkl-devel 2024.2.* - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 16148 @@ -6769,8 +6644,6 @@ packages: - liblapacke 3.9.0 26_win64_mkl - mkl >=2024.2.2,<2025.0a0 - mkl-devel 2024.2.* - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 18463 @@ -6794,8 +6667,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 3250841 @@ -6811,8 +6682,6 @@ packages: - lz4-c >=1.10.0,<1.11.0a0 - snappy >=1.2.1,<1.3.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 48427 @@ -6827,8 +6696,6 @@ packages: - lz4-c >=1.10.0,<1.11.0a0 - snappy >=1.2.1,<1.3.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 33602 @@ -6844,8 +6711,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 49840 @@ -6887,8 +6752,6 @@ packages: - libbrotlidec 1.1.0 hb9d3cd8_2 - libbrotlienc 1.1.0 hb9d3cd8_2 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 19264 @@ -6901,8 +6764,6 @@ packages: - brotli-bin 1.1.0 hd74edd7_2 - libbrotlidec 1.1.0 hd74edd7_2 - libbrotlienc 1.1.0 hd74edd7_2 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 19588 @@ -6917,8 +6778,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 19697 @@ -6931,8 +6790,6 @@ packages: - libbrotlidec 1.1.0 hb9d3cd8_2 - libbrotlienc 1.1.0 hb9d3cd8_2 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 18881 @@ -6944,8 +6801,6 @@ packages: - __osx >=11.0 - libbrotlidec 1.1.0 hd74edd7_2 - libbrotlienc 1.1.0 hd74edd7_2 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 16772 @@ -6959,8 +6814,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 20837 @@ -6976,8 +6829,6 @@ packages: - python_abi 3.12.* *_cp312 constrains: - libbrotlicommon 1.1.0 hb9d3cd8_2 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 349867 @@ -6993,8 +6844,6 @@ packages: - python_abi 3.12.* *_cp312 constrains: - libbrotlicommon 1.1.0 hd74edd7_2 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 339360 @@ -7010,8 +6859,6 @@ packages: - python_abi 3.13.* *_cp313 constrains: - libbrotlicommon 1.1.0 hd74edd7_2 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 339067 @@ -7027,8 +6874,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - libbrotlicommon 1.1.0 h2466b09_2 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 321874 @@ -7044,8 +6889,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - libbrotlicommon 1.1.0 h2466b09_2 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 322309 @@ -7056,8 +6899,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc-ng >=12 - arch: x86_64 - platform: linux license: bzip2-1.0.6 license_family: BSD size: 252783 @@ -7067,8 +6908,6 @@ packages: md5: fc6948412dbbbe9a4c9ddbbcfe0a79ab depends: - __osx >=11.0 - arch: arm64 - platform: osx license: bzip2-1.0.6 license_family: BSD size: 122909 @@ -7080,8 +6919,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: bzip2-1.0.6 license_family: BSD size: 54927 @@ -7092,8 +6929,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 206085 @@ -7103,8 +6938,6 @@ packages: md5: c1c999a38a4303b29d75c636eaa13cf9 depends: - __osx >=11.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 179496 @@ -7116,8 +6949,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 193862 @@ -7129,8 +6960,6 @@ packages: - binutils - gcc - gcc_linux-64 13.* - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 6085 @@ -7143,8 +6972,6 @@ packages: - clang_osx-arm64 17.* - ld64 >=530 - llvm-openmp - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 6220 @@ -7154,8 +6981,6 @@ packages: md5: 33c106164044a19c4e8d13277ae97c3f depends: - vs2019_win-64 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6513 @@ -7163,24 +6988,18 @@ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.12.14-hbcca054_0.conda sha256: 1afd7274cbc9a334d6d0bc62fa760acc7afdaceb0b91a8df370ec01fd75dc7dd md5: 720523eb0d6a9b0f6120c16b2aa4e7de - arch: x86_64 - platform: linux license: ISC size: 157088 timestamp: 1734208393264 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.12.14-hf0a4a13_0.conda sha256: 256be633fd0882ccc1a7a32bc278547e1703f85082c0789a87a603ee3ab8fb82 md5: 7cb381a6783d91902638e4ed1ebd478e - arch: arm64 - platform: osx license: ISC size: 157091 timestamp: 1734208344343 - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.12.14-h56e8100_0.conda sha256: 424d82db36cd26234bc4772426170efd60e888c2aed0099a257a95e131683a5e md5: cb2eaeb88549ddb27af533eccf9a45c1 - arch: x86_64 - platform: win license: ISC size: 157422 timestamp: 1734208404685 @@ -7225,8 +7044,6 @@ packages: - xorg-libx11 >=1.8.10,<2.0a0 - xorg-libxext >=1.3.6,<2.0a0 - xorg-libxrender >=0.9.11,<0.10.0a0 - arch: x86_64 - platform: linux license: LGPL-2.1-only or MPL-1.1 size: 978868 timestamp: 1733790976384 @@ -7237,8 +7054,6 @@ packages: - cctools_osx-arm64 1010.6 h623e0ac_2 - ld64 951.9 h39a299f_2 - libllvm17 >=17.0.6,<17.1.0a0 - arch: arm64 - platform: osx license: APSL-2.0 license_family: Other size: 21118 @@ -7258,8 +7073,6 @@ packages: - cctools 1010.6.* - ld64 951.9.* - clang 17.0.* - arch: arm64 - platform: osx license: APSL-2.0 license_family: Other size: 1101877 @@ -7282,8 +7095,6 @@ packages: - pycparser - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 294403 @@ -7298,8 +7109,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 281206 @@ -7314,8 +7123,6 @@ packages: - python >=3.13.0rc1,<3.14.0a0 - python >=3.13.0rc1,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 282115 @@ -7330,8 +7137,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 288142 @@ -7346,8 +7151,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 291828 @@ -7375,8 +7178,6 @@ packages: md5: c98bdbd4985530fac68ea4831d053ba1 depends: - clang-17 17.0.6 default_h146c034_7 - arch: arm64 - platform: osx license: Apache-2.0 WITH LLVM-exception license_family: Apache size: 24105 @@ -7394,8 +7195,6 @@ packages: - clang-tools 17.0.6 - clangdev 17.0.6 - llvm-tools 17.0.6 - arch: arm64 - platform: osx license: Apache-2.0 WITH LLVM-exception license_family: Apache size: 715930 @@ -7409,8 +7208,6 @@ packages: - compiler-rt 17.0.6.* - ld64_osx-arm64 - llvm-tools 17.0.6.* - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 17965 @@ -7420,8 +7217,6 @@ packages: md5: cf5bbfc8b558c41d2a4ba17f5cabb48c depends: - clang_impl_osx-arm64 17.0.6 he47c785_23 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 21177 @@ -7432,8 +7227,6 @@ packages: depends: - clang 17.0.6 default_h360f5da_7 - libcxx-devel 17.0.6.* - arch: arm64 - platform: osx license: Apache-2.0 WITH LLVM-exception license_family: Apache size: 24168 @@ -7446,8 +7239,6 @@ packages: - clangxx 17.0.6.* - libcxx >=17 - libllvm17 >=17.0.6,<17.1.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 18005 @@ -7458,8 +7249,6 @@ packages: depends: - clang_osx-arm64 17.0.6 h07b0088_23 - clangxx_impl_osx-arm64 17.0.6 h50f59cd_23 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 19581 @@ -7521,8 +7310,6 @@ packages: - clang 17.0.6.* - clangxx 17.0.6.* - compiler-rt_osx-arm64 17.0.6.* - arch: arm64 - platform: osx license: Apache-2.0 WITH LLVM-exception license_family: APACHE size: 94878 @@ -7549,8 +7336,6 @@ packages: - numpy >=1.23 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 276332 @@ -7565,8 +7350,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 245638 @@ -7581,8 +7364,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 216484 @@ -7614,8 +7395,6 @@ packages: - c-compiler 1.8.0 h2b85faf_1 - gxx - gxx_linux-64 13.* - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 6059 @@ -7626,8 +7405,6 @@ packages: depends: - c-compiler 1.8.0 hf48404e_1 - clangxx_osx-arm64 17.* - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 6261 @@ -7637,8 +7414,6 @@ packages: md5: 54d722a127a10b59596b5640d58f7ae6 depends: - vs2019_win-64 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6549 @@ -7661,8 +7436,6 @@ packages: - libstdcxx >=13 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 3228660 @@ -7676,8 +7449,6 @@ packages: - libstdcxx >=13 - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 3747300 @@ -7691,8 +7462,6 @@ packages: - libstdcxx >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 3752086 @@ -7706,8 +7475,6 @@ packages: - libstdcxx >=13 - python >=3.13.0rc2,<3.14.0a0 - python_abi 3.13.* *_cp313 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 3775368 @@ -7721,8 +7488,6 @@ packages: - libstdcxx >=13 - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 3219968 @@ -7736,8 +7501,6 @@ packages: - python >=3.10,<3.11.0a0 - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 2900787 @@ -7751,8 +7514,6 @@ packages: - python >=3.11,<3.12.0a0 - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 3436546 @@ -7766,8 +7527,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 3395577 @@ -7781,8 +7540,6 @@ packages: - python >=3.13.0rc2,<3.14.0a0 - python >=3.13.0rc2,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 3405225 @@ -7796,8 +7553,6 @@ packages: - python >=3.9,<3.10.0a0 - python >=3.9,<3.10.0a0 *_cpython - python_abi 3.9.* *_cp39 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 2911719 @@ -7811,8 +7566,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 2704725 @@ -7826,8 +7579,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 3232926 @@ -7841,8 +7592,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 3203995 @@ -7856,8 +7605,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 3199318 @@ -7871,8 +7618,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 2701290 @@ -7899,8 +7644,6 @@ packages: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - toolz >=0.10.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 394309 @@ -7914,8 +7657,6 @@ packages: - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - toolz >=0.10.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 338844 @@ -7930,8 +7671,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 316347 @@ -8025,8 +7764,6 @@ packages: md5: 418c6ca5929a611cbd69204907a83995 depends: - libgcc-ng >=12 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 760229 @@ -8034,8 +7771,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/dav1d-1.2.1-hb547adb_0.conda sha256: 93e077b880a85baec8227e8c72199220c7f87849ad32d02c14fb3807368260b8 md5: 5a74cdee497e6b65173e10d94582fae6 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 316394 @@ -8047,8 +7782,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 618643 @@ -8062,8 +7795,6 @@ packages: - libstdcxx >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 2668691 @@ -8077,8 +7808,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 2517686 @@ -8092,8 +7821,6 @@ packages: - python >=3.13,<3.14.0a0 - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 2568210 @@ -8107,8 +7834,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 3593695 @@ -8122,8 +7847,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 3614180 @@ -8285,8 +8008,6 @@ packages: - libgcc >=13 - libuuid >=2.38.1,<3.0a0 - libzlib >=1.3.1,<2.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 265599 @@ -8323,8 +8044,6 @@ packages: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - unicodedata2 >=15.1.0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 2883389 @@ -8340,8 +8059,6 @@ packages: - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - unicodedata2 >=15.1.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 2746515 @@ -8358,8 +8075,6 @@ packages: - unicodedata2 >=15.1.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 2452292 @@ -8423,8 +8138,6 @@ packages: - libgcc-ng >=12 - libpng >=1.6.39,<1.7.0a0 - libzlib >=1.2.13,<2.0.0a0 - arch: x86_64 - platform: linux license: GPL-2.0-only OR FTL size: 634972 timestamp: 1694615932610 @@ -8434,8 +8147,6 @@ packages: depends: - libpng >=1.6.39,<1.7.0a0 - libzlib >=1.2.13,<2.0.0a0 - arch: arm64 - platform: osx license: GPL-2.0-only OR FTL size: 596430 timestamp: 1694616332835 @@ -8448,8 +8159,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: GPL-2.0-only OR FTL size: 510306 timestamp: 1694616398888 @@ -8462,8 +8171,6 @@ packages: - libgcc >=13 - libiconv >=1.17,<2.0a0 - minizip >=4.0.7,<5.0a0 - arch: x86_64 - platform: linux license: MPL-1.1 license_family: MOZILLA size: 59299 @@ -8476,8 +8183,6 @@ packages: - libexpat >=2.6.4,<3.0a0 - libiconv >=1.17,<2.0a0 - minizip >=4.0.7,<5.0a0 - arch: arm64 - platform: osx license: MPL-1.1 license_family: MOZILLA size: 53378 @@ -8492,8 +8197,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MPL-1.1 license_family: MOZILLA size: 77528 @@ -8521,8 +8224,6 @@ packages: md5: 606924335b5bcdf90e9aed9a2f5d22ed depends: - gcc_impl_linux-64 13.3.0.* - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 53864 @@ -8538,8 +8239,6 @@ packages: - libsanitizer 13.3.0 heb74ff8_1 - libstdcxx >=13.3.0 - sysroot_linux-64 - arch: x86_64 - platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 67464415 @@ -8551,8 +8250,6 @@ packages: - binutils_linux-64 - gcc_impl_linux-64 13.3.0.* - sysroot_linux-64 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 32005 @@ -8593,8 +8290,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: LGPL-2.1-only size: 1869233 timestamp: 1725676083126 @@ -8604,8 +8299,6 @@ packages: depends: - __osx >=11.0 - libcxx >=17 - arch: arm64 - platform: osx license: LGPL-2.1-only size: 1481430 timestamp: 1725676193541 @@ -8616,8 +8309,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: LGPL-2.1-only size: 1665961 timestamp: 1725676536384 @@ -8633,8 +8324,6 @@ packages: - libzlib >=1.3.1,<2.0a0 - proj >=9.5.0,<9.6.0a0 - zlib - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 131394 @@ -8650,8 +8339,6 @@ packages: - libzlib >=1.3.1,<2.0a0 - proj >=9.5.0,<9.6.0a0 - zlib - arch: arm64 - platform: osx license: MIT license_family: MIT size: 113739 @@ -8668,8 +8355,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - zlib - arch: x86_64 - platform: win license: MIT license_family: MIT size: 123087 @@ -8681,8 +8366,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 119654 @@ -8693,8 +8376,6 @@ packages: depends: - __osx >=11.0 - libcxx >=17 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 82090 @@ -8704,8 +8385,6 @@ packages: md5: 3bf7b9fd5a7136126e0234db4b87c8b6 depends: - libgcc-ng >=12 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 77248 @@ -8713,8 +8392,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/giflib-5.2.2-h93a5062_0.conda sha256: 843b3f364ff844137e37d5c0a181f11f6d51adcedd216f019d074e5aa5d7e09c md5: 95fa1486c77505330c20f7202492b913 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 71613 @@ -8735,8 +8412,6 @@ packages: - gflags >=2.2.2,<2.3.0a0 - libgcc-ng >=12 - libstdcxx-ng >=12 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 143452 @@ -8748,8 +8423,6 @@ packages: - __osx >=11.0 - gflags >=2.2.2,<2.3.0a0 - libcxx >=16 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 112215 @@ -8760,8 +8433,6 @@ packages: depends: - libgcc-ng >=12 - libstdcxx-ng >=12 - arch: x86_64 - platform: linux license: GPL-2.0-or-later OR LGPL-3.0-or-later size: 460055 timestamp: 1718980856608 @@ -8771,8 +8442,6 @@ packages: depends: - __osx >=11.0 - libcxx >=16 - arch: arm64 - platform: osx license: GPL-2.0-or-later OR LGPL-3.0-or-later size: 365188 timestamp: 1718981343258 @@ -8782,8 +8451,6 @@ packages: depends: - libgcc-ng >=12 - libstdcxx-ng >=12 - arch: x86_64 - platform: linux license: LGPL-2.0-or-later license_family: LGPL size: 96855 @@ -8803,8 +8470,6 @@ packages: depends: - gcc 13.3.0.* - gxx_impl_linux-64 13.3.0.* - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 53338 @@ -8817,8 +8482,6 @@ packages: - libstdcxx-devel_linux-64 13.3.0 h84ea5a7_101 - sysroot_linux-64 - tzdata - arch: x86_64 - platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 13337720 @@ -8831,8 +8494,6 @@ packages: - gcc_linux-64 13.3.0 hc28eda2_7 - gxx_impl_linux-64 13.3.0.* - sysroot_linux-64 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 30356 @@ -8885,8 +8546,6 @@ packages: - libgcc >=13 - libglib >=2.82.2,<3.0a0 - libstdcxx >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 1600521 @@ -8943,8 +8602,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc-ng >=12 - libstdcxx-ng >=12 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 12129203 @@ -8954,8 +8611,6 @@ packages: md5: 5eb22c1d7b3fc4abb50d92d621583137 depends: - __osx >=11.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 11857802 @@ -9022,8 +8677,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda sha256: 0fd2b0b84c854029041b0ede8f4c2369242ee92acc0092f8407b1fe9238a8209 md5: 2d89243bfb53652c182a7c73182cce4f - arch: x86_64 - platform: win license: LicenseRef-IntelSimplifiedSoftwareOct2022 license_family: Proprietary size: 1852356 @@ -9196,8 +8849,6 @@ packages: - libstdcxx >=13 constrains: - jemalloc <0a0 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 55787 @@ -9211,8 +8862,6 @@ packages: - libjemalloc-local 5.3.0 hf9b8971_1 constrains: - jemalloc <0a0 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 55900 @@ -9243,8 +8892,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 82709 @@ -9254,8 +8901,6 @@ packages: md5: 94f14ef6157687c30feb44e1abecd577 depends: - __osx >=11.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 73715 @@ -9275,8 +8920,6 @@ packages: depends: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 17277 @@ -9288,8 +8931,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 17865 @@ -9300,8 +8941,6 @@ packages: depends: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 42235 @@ -9538,8 +9177,6 @@ packages: md5: 30186d27e2c9fa62b45fb1476b7200e3 depends: - libgcc-ng >=10.3.0 - arch: x86_64 - platform: linux license: LGPL-2.1-or-later size: 117831 timestamp: 1646151697040 @@ -9552,8 +9189,6 @@ packages: - libstdcxx >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 70922 @@ -9567,8 +9202,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 60966 @@ -9582,8 +9215,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 55405 @@ -9598,8 +9229,6 @@ packages: - libgcc-ng >=12 - libstdcxx-ng >=12 - openssl >=3.3.1,<4.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 1370023 @@ -9613,8 +9242,6 @@ packages: - libedit >=3.1.20191231,<3.2.0a0 - libedit >=3.1.20191231,<4.0a0 - openssl >=3.3.1,<4.0a0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 1155530 @@ -9627,8 +9254,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 712034 @@ -9640,8 +9265,6 @@ packages: - libgcc-ng >=12 - libjpeg-turbo >=3.0.0,<4.0a0 - libtiff >=4.6.0,<4.8.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 245247 @@ -9652,8 +9275,6 @@ packages: depends: - libjpeg-turbo >=3.0.0,<4.0a0 - libtiff >=4.6.0,<4.8.0a0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 211959 @@ -9667,8 +9288,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 507632 @@ -9682,8 +9301,6 @@ packages: constrains: - cctools 1010.6.* - cctools_osx-arm64 1010.6.* - arch: arm64 - platform: osx license: APSL-2.0 license_family: Other size: 18503 @@ -9702,8 +9319,6 @@ packages: - cctools_osx-arm64 1010.6.* - clang >=17.0.6,<18.0a0 - ld 951.9.* - arch: arm64 - platform: osx license: APSL-2.0 license_family: Other size: 1017788 @@ -9715,8 +9330,6 @@ packages: - __glibc >=2.17,<3.0.a0 constrains: - binutils_impl_linux-64 2.43 - arch: x86_64 - platform: linux license: GPL-3.0-only license_family: GPL size: 669211 @@ -9727,8 +9340,6 @@ packages: depends: - libgcc-ng >=12 - libstdcxx-ng >=12 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 281798 @@ -9738,8 +9349,6 @@ packages: md5: de462d5aacda3b30721b512c5da4e742 depends: - libcxx >=13.0.1 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 215721 @@ -9750,8 +9359,6 @@ packages: depends: - vc >=14.2,<15 - vs2015_runtime >=14.29.30037 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 194365 @@ -9775,8 +9382,6 @@ packages: constrains: - libabseil-static =20240722.0=cxx17* - abseil-cpp =20240722.0 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 1311599 @@ -9790,8 +9395,6 @@ packages: constrains: - libabseil-static =20240722.0=cxx17* - abseil-cpp =20240722.0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 1178260 @@ -9806,8 +9409,6 @@ packages: constrains: - abseil-cpp =20240722.0 - libabseil-static =20240722.0=cxx17* - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 1784929 @@ -9826,8 +9427,6 @@ packages: - lzo >=2.10,<3.0a0 - openssl >=3.4.0,<4.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 878021 @@ -9846,8 +9445,6 @@ packages: - lzo >=2.10,<3.0a0 - openssl >=3.4.0,<4.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 772780 @@ -9867,8 +9464,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 1082930 @@ -9908,8 +9503,6 @@ packages: - arrow-cpp <0.0a0 - parquet-cpp <0.0a0 - apache-arrow-proc =*=cpu - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 8770256 @@ -9947,8 +9540,6 @@ packages: - arrow-cpp <0.0a0 - parquet-cpp <0.0a0 - apache-arrow-proc =*=cpu - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 5506699 @@ -9984,8 +9575,6 @@ packages: - apache-arrow-proc =*=cpu - arrow-cpp <0.0a0 - parquet-cpp <0.0a0 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 5303299 @@ -9999,8 +9588,6 @@ packages: - libarrow 18.1.0 hd595efa_7_cpu - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 612463 @@ -10013,8 +9600,6 @@ packages: - __osx >=11.0 - libarrow 18.1.0 h0ad35bc_7_cpu - libcxx >=18 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 485185 @@ -10028,8 +9613,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.42.34433 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 447461 @@ -10045,8 +9628,6 @@ packages: - libgcc >=13 - libparquet 18.1.0 h081d1f1_7_cpu - libstdcxx >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 587497 @@ -10061,8 +9642,6 @@ packages: - libarrow-acero 18.1.0 hf07054f_7_cpu - libcxx >=18 - libparquet 18.1.0 h636d7b7_7_cpu - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 491237 @@ -10078,8 +9657,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.42.34433 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 435269 @@ -10098,8 +9675,6 @@ packages: - libgcc >=13 - libprotobuf >=5.28.3,<5.28.4.0a0 - libstdcxx >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 521861 @@ -10117,8 +9692,6 @@ packages: - libarrow-dataset 18.1.0 hf07054f_7_cpu - libcxx >=18 - libprotobuf >=5.28.3,<5.28.4.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 452385 @@ -10137,8 +9710,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.42.34433 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 365444 @@ -10153,8 +9724,6 @@ packages: - libgcc >=13 - rav1e >=0.6.6,<1.0a0 - svt-av1 >=2.3.0,<2.3.1.0a0 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 116202 @@ -10168,8 +9737,6 @@ packages: - dav1d >=1.2.1,<1.2.2.0a0 - rav1e >=0.6.6,<1.0a0 - svt-av1 >=2.3.0,<2.3.1.0a0 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 96781 @@ -10186,8 +9753,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 97828 @@ -10204,8 +9769,6 @@ packages: - libcblas 3.9.0 20_linux64_openblas - blas * openblas - liblapack 3.9.0 20_linux64_openblas - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 14433 @@ -10221,8 +9784,6 @@ packages: - blas * mkl - libcblas 3.9.0 26_linux64_mkl - liblapacke 3.9.0 26_linux64_mkl - arch: x86_64 - platform: linux track_features: - blas_mkl license: BSD-3-Clause @@ -10241,8 +9802,6 @@ packages: - liblapack 3.9.0 26_linux64_openblas - liblapacke 3.9.0 26_linux64_openblas - blas * openblas - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 16393 @@ -10259,8 +9818,6 @@ packages: - liblapacke 3.9.0 20_osxarm64_openblas - libcblas 3.9.0 20_osxarm64_openblas - blas * openblas - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 14722 @@ -10277,8 +9834,6 @@ packages: - liblapacke 3.9.0 26_osxarm64_openblas - libcblas 3.9.0 26_osxarm64_openblas - blas * openblas - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 16714 @@ -10292,8 +9847,6 @@ packages: constrains: - blas * blis - libcblas 3.9.0 26_win64_blis - arch: x86_64 - platform: win track_features: - blas_blis license: BSD-3-Clause @@ -10311,8 +9864,6 @@ packages: - liblapack 3.9.0 26_win64_mkl - blas * mkl - libcblas 3.9.0 26_win64_mkl - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 3733122 @@ -10328,8 +9879,6 @@ packages: - liblapacke 3.9.0 26_win64_openblas - blas * openblas - libcblas 3.9.0 26_win64_openblas - arch: x86_64 - platform: win track_features: - blas_openblas license: BSD-3-Clause @@ -10342,8 +9891,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 68851 @@ -10353,8 +9900,6 @@ packages: md5: d0bf1dff146b799b319ea0434b93f779 depends: - __osx >=11.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 68426 @@ -10366,8 +9911,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 70526 @@ -10379,8 +9922,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libbrotlicommon 1.1.0 hb9d3cd8_2 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 32696 @@ -10391,8 +9932,6 @@ packages: depends: - __osx >=11.0 - libbrotlicommon 1.1.0 hd74edd7_2 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 28378 @@ -10405,8 +9944,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 32685 @@ -10418,8 +9955,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libbrotlicommon 1.1.0 hb9d3cd8_2 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 281750 @@ -10430,8 +9965,6 @@ packages: depends: - __osx >=11.0 - libbrotlicommon 1.1.0 hd74edd7_2 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 279644 @@ -10444,8 +9977,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 245929 @@ -10460,8 +9991,6 @@ packages: - liblapacke 3.9.0 20_linux64_openblas - blas * openblas - liblapack 3.9.0 20_linux64_openblas - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 14383 @@ -10476,8 +10005,6 @@ packages: - liblapack 3.9.0 26_linux64_mkl - blas * mkl - liblapacke 3.9.0 26_linux64_mkl - arch: x86_64 - platform: linux track_features: - blas_mkl license: BSD-3-Clause @@ -10494,8 +10021,6 @@ packages: - liblapack 3.9.0 26_linux64_openblas - liblapacke 3.9.0 26_linux64_openblas - blas * openblas - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 16336 @@ -10510,8 +10035,6 @@ packages: - liblapack 3.9.0 20_osxarm64_openblas - liblapacke 3.9.0 20_osxarm64_openblas - blas * openblas - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 14642 @@ -10526,8 +10049,6 @@ packages: - liblapack 3.9.0 26_osxarm64_openblas - liblapacke 3.9.0 26_osxarm64_openblas - blas * openblas - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 16628 @@ -10540,8 +10061,6 @@ packages: - libblas 3.9.0 26_win64_blis constrains: - blas * blis - arch: x86_64 - platform: win track_features: - blas_blis license: BSD-3-Clause @@ -10558,8 +10077,6 @@ packages: - liblapacke 3.9.0 26_win64_mkl - liblapack 3.9.0 26_win64_mkl - blas * mkl - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 3732146 @@ -10575,8 +10092,6 @@ packages: - liblapack 3.9.0 26_win64_openblas - liblapacke 3.9.0 26_win64_openblas - blas * openblas - arch: x86_64 - platform: win track_features: - blas_openblas license: BSD-3-Clause @@ -10590,8 +10105,6 @@ packages: - __osx >=11.0 - libcxx >=17.0.6 - libllvm17 >=17.0.6,<17.1.0a0 - arch: arm64 - platform: osx license: Apache-2.0 WITH LLVM-exception license_family: Apache size: 12408943 @@ -10602,8 +10115,6 @@ packages: depends: - libgcc-ng >=9.4.0 - libstdcxx-ng >=9.4.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 20440 @@ -10613,8 +10124,6 @@ packages: md5: 32bd82a6a625ea6ce090a81c3d34edeb depends: - libcxx >=11.1.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 18765 @@ -10625,8 +10134,6 @@ packages: depends: - vc >=14.1,<15.0a0 - vs2015_runtime >=14.16.27012 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 25694 @@ -10639,8 +10146,6 @@ packages: - libgcc-ng >=12 - libstdcxx-ng >=12 - libzlib >=1.2.13,<2.0.0a0 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 4519402 @@ -10657,8 +10162,6 @@ packages: - libzlib >=1.3.1,<2.0a0 - openssl >=3.4.0,<4.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: linux license: curl license_family: MIT size: 423011 @@ -10674,8 +10177,6 @@ packages: - libzlib >=1.3.1,<2.0a0 - openssl >=3.4.0,<4.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: arm64 - platform: osx license: curl license_family: MIT size: 385098 @@ -10690,8 +10191,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: curl license_family: MIT size: 349553 @@ -10701,8 +10200,6 @@ packages: md5: ce5252d8db110cdb4ae4173d0a63c7c5 depends: - __osx >=11.0 - arch: arm64 - platform: osx license: Apache-2.0 WITH LLVM-exception license_family: Apache size: 520992 @@ -10712,8 +10209,6 @@ packages: md5: 555639d6c7a4c6838cec6e50453fea43 depends: - libcxx >=17.0.6 - arch: arm64 - platform: osx license: Apache-2.0 WITH LLVM-exception license_family: Apache size: 820887 @@ -10724,8 +10219,6 @@ packages: depends: - libgcc-ng >=12 - libstdcxx-ng >=12 - arch: x86_64 - platform: linux license: LGPL-3.0-or-later license_family: LGPL size: 411814 @@ -10735,8 +10228,6 @@ packages: md5: 7c718ee6d8497702145612fa0898a12d depends: - libcxx >=15 - arch: arm64 - platform: osx license: LGPL-3.0-or-later license_family: LGPL size: 277861 @@ -10748,8 +10239,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: LGPL-3.0-or-later license_family: LGPL size: 252968 @@ -10760,8 +10249,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 72255 @@ -10771,8 +10258,6 @@ packages: md5: 1d8b9588be14e71df38c525767a1ac30 depends: - __osx >=11.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 54132 @@ -10784,8 +10269,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 155723 @@ -10796,8 +10279,6 @@ packages: depends: - libgcc-ng >=7.5.0 - ncurses >=6.2,<7.0.0a0 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 123878 @@ -10807,8 +10288,6 @@ packages: md5: 30e4362988a2623e9eb34337b83e01f9 depends: - ncurses >=6.2,<7.0.0a0 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 96607 @@ -10818,8 +10297,6 @@ packages: md5: 172bf1cd1ff8629f2b1179945ed45055 depends: - libgcc-ng >=12 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 112766 @@ -10827,8 +10304,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f md5: 36d33e440c31857372a72137f78bacf5 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 107458 @@ -10839,8 +10314,6 @@ packages: depends: - libgcc-ng >=12 - openssl >=3.1.1,<4.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 427426 @@ -10850,8 +10323,6 @@ packages: md5: 1a109764bff3bdc7bdd84088347d71dc depends: - openssl >=3.1.1,<4.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 368167 @@ -10864,8 +10335,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 410555 @@ -10878,8 +10347,6 @@ packages: - libgcc >=13 constrains: - expat 2.6.4.* - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 73304 @@ -10891,8 +10358,6 @@ packages: - __osx >=11.0 constrains: - expat 2.6.4.* - arch: arm64 - platform: osx license: MIT license_family: MIT size: 64693 @@ -10906,8 +10371,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - expat 2.6.4.* - arch: x86_64 - platform: win license: MIT license_family: MIT size: 139068 @@ -10917,8 +10380,6 @@ packages: md5: d645c6d2ac96843a2bfaccd2d62b3ac3 depends: - libgcc-ng >=9.4.0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 58292 @@ -10926,8 +10387,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 sha256: 41b3d13efb775e340e4dba549ab5c029611ea6918703096b2eaa9c015c0750ca md5: 086914b672be056eb70fd4285b6783b6 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 39020 @@ -10938,8 +10397,6 @@ packages: depends: - vc >=14.1,<15.0a0 - vs2015_runtime >=14.16.27012 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 42063 @@ -10953,8 +10410,6 @@ packages: constrains: - libgomp 14.2.0 h77fa898_1 - libgcc-ng ==14.2.0=*_1 - arch: x86_64 - platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 848745 @@ -10969,8 +10424,6 @@ packages: - libgcc-ng ==14.2.0=*_1 - libgomp 14.2.0 h1383e82_1 - msys2-conda-epoch <0.0a0 - arch: x86_64 - platform: win license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 666386 @@ -10989,8 +10442,6 @@ packages: md5: e39480b9ca41323497b05492a63bc35b depends: - libgcc 14.2.0 h77fa898_1 - arch: x86_64 - platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 54142 @@ -11033,8 +10484,6 @@ packages: - zstd >=1.5.6,<1.6.0a0 constrains: - libgdal 3.10.0.* - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 10791475 @@ -11075,8 +10524,6 @@ packages: - zstd >=1.5.6,<1.6.0a0 constrains: - libgdal 3.10.0.* - arch: arm64 - platform: osx license: MIT license_family: MIT size: 8495760 @@ -11116,8 +10563,6 @@ packages: - zstd >=1.5.6,<1.6.0a0 constrains: - libgdal 3.10.0.* - arch: x86_64 - platform: win license: MIT license_family: MIT size: 8426143 @@ -11129,8 +10574,6 @@ packages: - libgfortran5 14.2.0 hd5240d6_1 constrains: - libgfortran-ng ==14.2.0=*_1 - arch: x86_64 - platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 53997 @@ -11140,8 +10583,6 @@ packages: md5: 4a55d9e169114b2b90d3ec4604cd7bbf depends: - libgfortran5 13.2.0 hf226fd6_3 - arch: arm64 - platform: osx license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 110233 @@ -11151,8 +10592,6 @@ packages: md5: 0a7f4cd238267c88e5d69f7826a407eb depends: - libgfortran 14.2.0 h69a702a_1 - arch: x86_64 - platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 54106 @@ -11164,8 +10603,6 @@ packages: - libgcc >=14.2.0 constrains: - libgfortran 14.2.0 - arch: x86_64 - platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 1462645 @@ -11177,8 +10614,6 @@ packages: - llvm-openmp >=8.0.0 constrains: - libgfortran 5.0.0 13_2_0_*_3 - arch: arm64 - platform: osx license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 997381 @@ -11195,8 +10630,6 @@ packages: - pcre2 >=10.44,<10.45.0a0 constrains: - glib 2.82.2 *_0 - arch: x86_64 - platform: linux license: LGPL-2.1-or-later size: 3931898 timestamp: 1729191404130 @@ -11205,8 +10638,6 @@ packages: md5: cc3573974587f12dda90d96e3e55a702 depends: - _libgcc_mutex 0.1 conda_forge - arch: x86_64 - platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 460992 @@ -11218,8 +10649,6 @@ packages: - libwinpthread >=12.0.0.r4.gg4f2fc60ca constrains: - msys2-conda-epoch <0.0a0 - arch: x86_64 - platform: win license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 524249 @@ -11239,8 +10668,6 @@ packages: - openssl >=3.4.0,<4.0a0 constrains: - libgoogle-cloud 2.33.0 *_1 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 1254656 @@ -11259,8 +10686,6 @@ packages: - openssl >=3.4.0,<4.0a0 constrains: - libgoogle-cloud 2.33.0 *_1 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 877594 @@ -11279,8 +10704,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - libgoogle-cloud 2.33.0 *_1 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 14439 @@ -11298,8 +10721,6 @@ packages: - libstdcxx >=13 - libzlib >=1.3.1,<2.0a0 - openssl - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 784357 @@ -11316,8 +10737,6 @@ packages: - libgoogle-cloud 2.33.0 hdbe95d5_1 - libzlib >=1.3.1,<2.0a0 - openssl - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 526963 @@ -11334,8 +10753,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 14336 @@ -11357,8 +10774,6 @@ packages: - re2 constrains: - grpc-cpp =1.67.1 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 7792251 @@ -11379,8 +10794,6 @@ packages: - re2 constrains: - grpc-cpp =1.67.1 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 5311706 @@ -11402,8 +10815,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - grpc-cpp =1.67.1 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 17282979 @@ -11420,8 +10831,6 @@ packages: - libgcc >=13 - libstdcxx >=13 - x265 >=3.5,<3.6.0a0 - arch: x86_64 - platform: linux license: LGPL-3.0-or-later license_family: LGPL size: 588609 @@ -11437,8 +10846,6 @@ packages: - libcxx >=18 - libde265 >=1.0.15,<1.0.16.0a0 - x265 >=3.5,<3.6.0a0 - arch: arm64 - platform: osx license: LGPL-3.0-or-later license_family: LGPL size: 429678 @@ -11455,8 +10862,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - x265 >=3.5,<3.6.0a0 - arch: x86_64 - platform: win license: LGPL-3.0-or-later license_family: LGPL size: 388187 @@ -11469,8 +10874,6 @@ packages: - libgcc >=13 - libstdcxx >=13 - libxml2 >=2.13.4,<3.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 2423200 @@ -11484,8 +10887,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 2390021 @@ -11495,16 +10896,12 @@ packages: md5: d66573916ffcf376178462f1b61c941e depends: - libgcc-ng >=12 - arch: x86_64 - platform: linux license: LGPL-2.1-only size: 705775 timestamp: 1702682170569 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.17-h0d3ecfb_2.conda sha256: bc7de5097b97bcafcf7deaaed505f7ce02f648aac8eccc0d5a47cc599a1d0304 md5: 69bda57310071cf6d2b86caf11573d2d - arch: arm64 - platform: osx license: LGPL-2.1-only size: 676469 timestamp: 1702682458114 @@ -11515,8 +10912,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: LGPL-2.1-only size: 636146 timestamp: 1702682547199 @@ -11527,8 +10922,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 1500855 @@ -11539,8 +10932,6 @@ packages: depends: - __osx >=11.0 - libcxx >=17 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 186656 @@ -11552,8 +10943,6 @@ packages: - libgcc-ng >=12 constrains: - jpeg <0.0.0a - arch: x86_64 - platform: linux license: IJG AND BSD-3-Clause AND Zlib size: 618575 timestamp: 1694474974816 @@ -11562,8 +10951,6 @@ packages: md5: 3ff1e053dc3a2b8e36b9bfa4256a58d1 constrains: - jpeg <0.0.0a - arch: arm64 - platform: osx license: IJG AND BSD-3-Clause AND Zlib size: 547541 timestamp: 1694475104253 @@ -11576,8 +10963,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - jpeg <0.0.0a - arch: x86_64 - platform: win license: IJG AND BSD-3-Clause AND Zlib size: 822966 timestamp: 1694475223854 @@ -11591,8 +10976,6 @@ packages: - libstdcxx-ng >=13 - libzlib >=1.3.1,<2.0a0 - uriparser >=0.9.8,<1.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 402219 @@ -11606,8 +10989,6 @@ packages: - libexpat >=2.6.2,<3.0a0 - libzlib >=1.3.1,<2.0a0 - uriparser >=0.9.8,<1.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 281362 @@ -11622,8 +11003,6 @@ packages: - uriparser >=0.9.8,<1.0a0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 1651104 @@ -11638,8 +11017,6 @@ packages: - liblapacke 3.9.0 20_linux64_openblas - libcblas 3.9.0 20_linux64_openblas - blas * openblas - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 14350 @@ -11654,8 +11031,6 @@ packages: - blas * mkl - libcblas 3.9.0 26_linux64_mkl - liblapacke 3.9.0 26_linux64_mkl - arch: x86_64 - platform: linux track_features: - blas_mkl license: BSD-3-Clause @@ -11672,8 +11047,6 @@ packages: - libcblas 3.9.0 26_linux64_openblas - liblapacke 3.9.0 26_linux64_openblas - blas * openblas - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 16338 @@ -11688,8 +11061,6 @@ packages: - liblapacke 3.9.0 20_osxarm64_openblas - libcblas 3.9.0 20_osxarm64_openblas - blas * openblas - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 14648 @@ -11704,8 +11075,6 @@ packages: - liblapacke 3.9.0 26_osxarm64_openblas - libcblas 3.9.0 26_osxarm64_openblas - blas * openblas - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 16624 @@ -11720,8 +11089,6 @@ packages: - liblapacke 3.9.0 26_win64_mkl - blas * mkl - libcblas 3.9.0 26_win64_mkl - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 3732160 @@ -11737,8 +11104,6 @@ packages: - liblapacke 3.9.0 26_win64_openblas - blas * openblas - libcblas 3.9.0 26_win64_openblas - arch: x86_64 - platform: win track_features: - blas_openblas license: BSD-3-Clause @@ -11754,8 +11119,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win track_features: - blas_netlib blas_netlib_2 license: BSD-3-Clause @@ -11772,8 +11135,6 @@ packages: - liblapack 3.9.0 26_linux64_mkl constrains: - blas * mkl - arch: x86_64 - platform: linux track_features: - blas_mkl license: BSD-3-Clause @@ -11790,8 +11151,6 @@ packages: - liblapack 3.9.0 26_win64_mkl constrains: - blas * mkl - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 3732139 @@ -11803,8 +11162,6 @@ packages: - libgcc-ng >=12 - libstdcxx-ng >=12 - libzlib >=1.2.13,<2.0.0a0 - arch: x86_64 - platform: linux license: Apache-2.0 WITH LLVM-exception license_family: Apache size: 31484415 @@ -11815,8 +11172,6 @@ packages: depends: - libcxx >=15 - libzlib >=1.2.13,<2.0.0a0 - arch: arm64 - platform: osx license: Apache-2.0 WITH LLVM-exception license_family: Apache size: 20571387 @@ -11830,8 +11185,6 @@ packages: - libxml2 >=2.12.7,<3.0a0 - libzlib >=1.3.1,<2.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: arm64 - platform: osx license: Apache-2.0 WITH LLVM-exception license_family: Apache size: 24612870 @@ -11842,8 +11195,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: 0BSD size: 111132 timestamp: 1733407410083 @@ -11852,8 +11203,6 @@ packages: md5: b2553114a7f5e20ccd02378a77d836aa depends: - __osx >=11.0 - arch: arm64 - platform: osx license: 0BSD size: 99129 timestamp: 1733407496073 @@ -11864,8 +11213,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: 0BSD size: 104332 timestamp: 1733407872569 @@ -11875,8 +11222,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc-ng >=12 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 89991 @@ -11886,8 +11231,6 @@ packages: md5: 7476305c35dd9acef48da8f754eedb40 depends: - __osx >=11.0 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 69263 @@ -11899,8 +11242,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 88657 @@ -11917,8 +11258,6 @@ packages: - libstdcxx >=13 - libzlib >=1.3.1,<2.0a0 - openssl >=3.3.2,<4.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 647599 @@ -11934,8 +11273,6 @@ packages: - libev >=4.33,<5.0a0 - libzlib >=1.3.1,<2.0a0 - openssl >=3.3.2,<4.0a0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 566719 @@ -11945,8 +11282,6 @@ packages: md5: 30fd6e37fe21f86f4bd26d6ee73eeec7 depends: - libgcc-ng >=12 - arch: x86_64 - platform: linux license: LGPL-2.1-only license_family: GPL size: 33408 @@ -11960,8 +11295,6 @@ packages: - libgfortran5 >=12.3.0 constrains: - openblas >=0.3.25,<0.3.26.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 5545169 @@ -11976,8 +11309,6 @@ packages: - libgfortran5 >=14.2.0 constrains: - openblas >=0.3.28,<0.3.29.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 5578513 @@ -11991,8 +11322,6 @@ packages: - llvm-openmp >=16.0.6 constrains: - openblas >=0.3.25,<0.3.26.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 2896390 @@ -12007,8 +11336,6 @@ packages: - llvm-openmp >=18.1.8 constrains: - openblas >=0.3.28,<0.3.29.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 4165774 @@ -12022,8 +11349,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - openblas >=0.3.28,<0.3.29.0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 3935890 @@ -12039,8 +11364,6 @@ packages: - libstdcxx >=13 - libthrift >=0.21.0,<0.21.1.0a0 - openssl >=3.4.0,<4.0a0 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 1205598 @@ -12055,8 +11378,6 @@ packages: - libcxx >=18 - libthrift >=0.21.0,<0.21.1.0a0 - openssl >=3.4.0,<4.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 873251 @@ -12072,8 +11393,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.42.34433 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 812887 @@ -12085,8 +11404,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libzlib >=1.3.1,<2.0a0 - arch: x86_64 - platform: linux license: zlib-acknowledgement size: 290661 timestamp: 1726234747153 @@ -12096,8 +11413,6 @@ packages: depends: - __osx >=11.0 - libzlib >=1.3.1,<2.0a0 - arch: arm64 - platform: osx license: zlib-acknowledgement size: 263385 timestamp: 1726234714421 @@ -12109,8 +11424,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: zlib-acknowledgement size: 348933 timestamp: 1726235196095 @@ -12124,8 +11437,6 @@ packages: - libgcc >=13 - libstdcxx >=13 - libzlib >=1.3.1,<2.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 2960815 @@ -12139,8 +11450,6 @@ packages: - libabseil >=20240722.0,<20240723.0a0 - libcxx >=18 - libzlib >=1.3.1,<2.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 2271580 @@ -12155,8 +11464,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6172959 @@ -12191,8 +11498,6 @@ packages: - libstdcxx >=13 constrains: - re2 2024.07.02.* - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 209793 @@ -12207,8 +11512,6 @@ packages: - libcxx >=18 constrains: - re2 2024.07.02.* - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 167155 @@ -12224,8 +11527,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - re2 2024.07.02.* - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 260655 @@ -12238,8 +11539,6 @@ packages: - geos >=3.13.0,<3.13.1.0a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: GPL-2.0-or-later license_family: GPL size: 231770 @@ -12251,8 +11550,6 @@ packages: - __osx >=11.0 - geos >=3.13.0,<3.13.1.0a0 - libcxx >=17 - arch: arm64 - platform: osx license: GPL-2.0-or-later license_family: GPL size: 191064 @@ -12265,8 +11562,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: GPL-2.0-or-later license_family: GPL size: 404515 @@ -12277,8 +11572,6 @@ packages: depends: - libgcc >=13.3.0 - libstdcxx >=13.3.0 - arch: x86_64 - platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 4133922 @@ -12288,8 +11581,6 @@ packages: md5: a587892d3c13b6621a6091be690dbca2 depends: - libgcc-ng >=12 - arch: x86_64 - platform: linux license: ISC size: 205978 timestamp: 1716828628198 @@ -12298,8 +11589,6 @@ packages: md5: a7ce36e284c5faaf93c220dfc39e3abd depends: - __osx >=11.0 - arch: arm64 - platform: osx license: ISC size: 164972 timestamp: 1716828607917 @@ -12310,8 +11599,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: ISC size: 202344 timestamp: 1716828757533 @@ -12332,8 +11619,6 @@ packages: - proj >=9.5.1,<9.6.0a0 - sqlite - zlib - arch: x86_64 - platform: linux license: MPL-1.1 license_family: MOZILLA size: 4033736 @@ -12355,8 +11640,6 @@ packages: - proj >=9.5.1,<9.6.0a0 - sqlite - zlib - arch: arm64 - platform: osx license: MPL-1.1 license_family: MOZILLA size: 2943606 @@ -12378,8 +11661,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - zlib - arch: x86_64 - platform: win license: MPL-1.1 license_family: MOZILLA size: 8715367 @@ -12391,8 +11672,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libzlib >=1.3.1,<2.0a0 - arch: x86_64 - platform: linux license: Unlicense size: 873551 timestamp: 1733761824646 @@ -12402,8 +11681,6 @@ packages: depends: - __osx >=11.0 - libzlib >=1.3.1,<2.0a0 - arch: arm64 - platform: osx license: Unlicense size: 850553 timestamp: 1733762057506 @@ -12414,8 +11691,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Unlicense size: 891292 timestamp: 1733762116902 @@ -12427,8 +11702,6 @@ packages: - libgcc >=13 - libzlib >=1.3.1,<2.0a0 - openssl >=3.4.0,<4.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 304278 @@ -12439,8 +11712,6 @@ packages: depends: - libzlib >=1.3.1,<2.0a0 - openssl >=3.4.0,<4.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 279028 @@ -12454,8 +11725,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 291889 @@ -12465,8 +11734,6 @@ packages: md5: 234a5554c53625688d51062645337328 depends: - libgcc 14.2.0 h77fa898_1 - arch: x86_64 - platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 3893695 @@ -12485,8 +11752,6 @@ packages: md5: 8371ac6457591af2cf6159439c1fd051 depends: - libstdcxx 14.2.0 hc0a3c3a_1 - arch: x86_64 - platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL size: 54105 @@ -12501,8 +11766,6 @@ packages: - libstdcxx >=13 - libzlib >=1.3.1,<2.0a0 - openssl >=3.3.2,<4.0a0 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 425773 @@ -12516,8 +11779,6 @@ packages: - libevent >=2.1.12,<2.1.13.0a0 - libzlib >=1.3.1,<2.0a0 - openssl >=3.3.2,<4.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 324342 @@ -12532,8 +11793,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 633857 @@ -12552,8 +11811,6 @@ packages: - libwebp-base >=1.4.0,<2.0a0 - libzlib >=1.3.1,<2.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: linux license: HPND size: 428173 timestamp: 1734398813264 @@ -12570,8 +11827,6 @@ packages: - libwebp-base >=1.4.0,<2.0a0 - libzlib >=1.3.1,<2.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: arm64 - platform: osx license: HPND size: 370600 timestamp: 1734398863052 @@ -12588,8 +11843,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: win license: HPND size: 978878 timestamp: 1734399004259 @@ -12599,8 +11852,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 81500 @@ -12610,8 +11861,6 @@ packages: md5: f777470d31c78cd0abe1903a2fda436f depends: - __osx >=11.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 83000 @@ -12623,8 +11872,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 84392 @@ -12634,8 +11881,6 @@ packages: md5: 40b61aab5c7ba9ff276c41cfffe6b80b depends: - libgcc-ng >=12 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 33601 @@ -12648,8 +11893,6 @@ packages: - libgcc >=13 constrains: - libwebp 1.5.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 429973 @@ -12661,8 +11904,6 @@ packages: - __osx >=11.0 constrains: - libwebp 1.5.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 290013 @@ -12676,8 +11917,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - libwebp 1.5.0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 273661 @@ -12690,8 +11929,6 @@ packages: constrains: - pthreads-win32 <0.0a0 - msys2-conda-epoch <0.0a0 - arch: x86_64 - platform: win license: MIT AND BSD-3-Clause-Clear size: 35433 timestamp: 1724681489463 @@ -12704,8 +11941,6 @@ packages: - pthread-stubs - xorg-libxau >=1.0.11,<2.0a0 - xorg-libxdmcp - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 395888 @@ -12718,8 +11953,6 @@ packages: - pthread-stubs - xorg-libxau >=1.0.11,<2.0a0 - xorg-libxdmcp - arch: arm64 - platform: osx license: MIT license_family: MIT size: 323658 @@ -12734,8 +11967,6 @@ packages: - ucrt >=10.0.20348.0 - xorg-libxau >=1.0.11,<2.0a0 - xorg-libxdmcp - arch: x86_64 - platform: win license: MIT license_family: MIT size: 1208687 @@ -12745,8 +11976,6 @@ packages: md5: 5aa797f8787fe7a17d1b0821485b5adc depends: - libgcc-ng >=12 - arch: x86_64 - platform: linux license: LGPL-2.1-or-later size: 100393 timestamp: 1702724383534 @@ -12761,8 +11990,6 @@ packages: - libzlib >=1.3.1,<2.0a0 constrains: - icu <0.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 689993 @@ -12777,8 +12004,6 @@ packages: - libiconv >=1.17,<2.0a0 - liblzma >=5.6.3,<6.0a0 - libzlib >=1.3.1,<2.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 690589 @@ -12792,8 +12017,6 @@ packages: - libiconv >=1.17,<2.0a0 - liblzma >=5.6.3,<6.0a0 - libzlib >=1.3.1,<2.0a0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 582898 @@ -12807,8 +12030,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 1612294 @@ -12821,8 +12042,6 @@ packages: - libgcc >=13 constrains: - zlib 1.3.1 *_2 - arch: x86_64 - platform: linux license: Zlib license_family: Other size: 60963 @@ -12834,8 +12053,6 @@ packages: - __osx >=11.0 constrains: - zlib 1.3.1 *_2 - arch: arm64 - platform: osx license: Zlib license_family: Other size: 46438 @@ -12849,8 +12066,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - zlib 1.3.1 *_2 - arch: x86_64 - platform: win license: Zlib license_family: Other size: 55476 @@ -12883,8 +12098,6 @@ packages: constrains: - ipython >=8.14.0 - rich >=12.3.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 161888 @@ -12901,8 +12114,6 @@ packages: constrains: - ipython >=8.14.0 - rich >=12.3.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 149435 @@ -12919,8 +12130,6 @@ packages: constrains: - rich >=12.3.0 - ipython >=8.14.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 151375 @@ -12937,8 +12146,6 @@ packages: constrains: - rich >=12.3.0 - ipython >=8.14.0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 169720 @@ -12950,8 +12157,6 @@ packages: - __glibc >=2.17,<3.0.a0 constrains: - openmp 19.1.6|19.1.6.* - arch: x86_64 - platform: linux license: Apache-2.0 WITH LLVM-exception license_family: APACHE size: 3201572 @@ -12963,8 +12168,6 @@ packages: - __osx >=11.0 constrains: - openmp 19.1.6|19.1.6.* - arch: arm64 - platform: osx license: Apache-2.0 WITH LLVM-exception license_family: APACHE size: 281251 @@ -12983,8 +12186,6 @@ packages: - llvm 17.0.6 - llvmdev 17.0.6 - clang 17.0.6 - arch: arm64 - platform: osx license: Apache-2.0 WITH LLVM-exception license_family: Apache size: 21864486 @@ -13000,8 +12201,6 @@ packages: - libzlib >=1.3.1,<2.0a0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 3442782 @@ -13017,8 +12216,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 370106 @@ -13034,8 +12231,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - vs2015_runtime - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 17112697 @@ -13058,8 +12253,6 @@ packages: - lz4-c >=1.10.0,<1.11.0a0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 39147 @@ -13073,8 +12266,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 105517 @@ -13089,8 +12280,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 42400 @@ -13102,8 +12291,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 167055 @@ -13114,8 +12301,6 @@ packages: depends: - __osx >=11.0 - libcxx >=18 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 148824 @@ -13127,8 +12312,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 139891 @@ -13138,8 +12321,6 @@ packages: md5: ec7398d21e2651e0dcb0044d03b9a339 depends: - libgcc-ng >=12 - arch: x86_64 - platform: linux license: GPL-2.0-or-later license_family: GPL2 size: 171416 @@ -13147,8 +12328,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lzo-2.10-h93a5062_1001.conda sha256: b68160b0a8ec374cea12de7afb954ca47419cdc300358232e19cec666d60b929 md5: 915996063a7380c652f83609e970c2a7 - arch: arm64 - platform: osx license: GPL-2.0-or-later license_family: GPL2 size: 131447 @@ -13160,8 +12339,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: GPL-2.0-or-later license_family: GPL2 size: 142771 @@ -13172,8 +12349,6 @@ packages: depends: - m2w64-gcc-libs-core - msys2-conda-epoch ==20160418 - arch: x86_64 - platform: win license: GPL, LGPL, FDL, custom size: 350687 timestamp: 1608163451316 @@ -13186,8 +12361,6 @@ packages: - m2w64-gmp - m2w64-libwinpthread-git - msys2-conda-epoch ==20160418 - arch: x86_64 - platform: win license: GPL3+, partial:GCCRLE, partial:LGPL2+ size: 532390 timestamp: 1608163512830 @@ -13198,8 +12371,6 @@ packages: - m2w64-gmp - m2w64-libwinpthread-git - msys2-conda-epoch ==20160418 - arch: x86_64 - platform: win license: GPL3+, partial:GCCRLE, partial:LGPL2+ size: 219240 timestamp: 1608163481341 @@ -13208,8 +12379,6 @@ packages: md5: 53a1c73e1e3d185516d7e3af177596d9 depends: - msys2-conda-epoch ==20160418 - arch: x86_64 - platform: win license: LGPL3 size: 743501 timestamp: 1608163782057 @@ -13218,8 +12387,6 @@ packages: md5: 774130a326dee16f1ceb05cc687ee4f0 depends: - msys2-conda-epoch ==20160418 - arch: x86_64 - platform: win license: MIT, BSD size: 31928 timestamp: 1608166099896 @@ -13229,8 +12396,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: GPL-3.0-or-later license_family: GPL size: 513088 @@ -13240,8 +12405,6 @@ packages: md5: 9f44ef1fea0a25d6a3491c58f3af8460 depends: - __osx >=11.0 - arch: arm64 - platform: osx license: GPL-3.0-or-later license_family: GPL size: 274048 @@ -13253,8 +12416,6 @@ packages: - libgcc >=13 - libwinpthread >=12.0.0.r4.gg4f2fc60ca - ucrt >=10.0.20348.0 - arch: x86_64 - platform: win license: GPL-3.0-or-later license_family: GPL size: 2176937 @@ -13304,8 +12465,6 @@ packages: - python_abi 3.12.* *_cp312 constrains: - jinja2 >=3.0.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 24604 @@ -13320,8 +12479,6 @@ packages: - python_abi 3.13.* *_cp313 constrains: - jinja2 >=3.0.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 24856 @@ -13336,8 +12493,6 @@ packages: - python_abi 3.12.* *_cp312 constrains: - jinja2 >=3.0.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 24048 @@ -13352,8 +12507,6 @@ packages: - python_abi 3.13.* *_cp313 constrains: - jinja2 >=3.0.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 24757 @@ -13369,8 +12522,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - jinja2 >=3.0.0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 27582 @@ -13386,8 +12537,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - jinja2 >=3.0.0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 27930 @@ -13414,8 +12563,6 @@ packages: - python_abi 3.12.* *_cp312 - qhull >=2020.2,<2020.3.0a0 - tk >=8.6.13,<8.7.0a0 - arch: x86_64 - platform: linux license: PSF-2.0 license_family: PSF size: 8210655 @@ -13441,8 +12588,6 @@ packages: - python-dateutil >=2.7 - python_abi 3.12.* *_cp312 - qhull >=2020.2,<2020.3.0a0 - arch: arm64 - platform: osx license: PSF-2.0 license_family: PSF size: 8019543 @@ -13468,8 +12613,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: PSF-2.0 license_family: PSF size: 8012369 @@ -13540,8 +12683,6 @@ packages: - libzlib >=1.3.1,<2.0a0 - openssl >=3.4.0,<4.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: linux license: Zlib license_family: Other size: 92332 @@ -13558,8 +12699,6 @@ packages: - libzlib >=1.3.1,<2.0a0 - openssl >=3.4.0,<4.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: arm64 - platform: osx license: Zlib license_family: Other size: 77597 @@ -13575,8 +12714,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: win license: Zlib license_family: Other size: 85799 @@ -13599,8 +12736,6 @@ packages: - _openmp_mutex >=4.5 - llvm-openmp >=19.1.2 - tbb 2021.* - arch: x86_64 - platform: linux license: LicenseRef-IntelSimplifiedSoftwareOct2022 license_family: Proprietary size: 124718448 @@ -13611,8 +12746,6 @@ packages: depends: - intel-openmp 2024.* - tbb 2021.* - arch: x86_64 - platform: win license: LicenseRef-IntelSimplifiedSoftwareOct2022 license_family: Proprietary size: 103106385 @@ -13623,8 +12756,6 @@ packages: depends: - mkl 2024.2.2 ha957f24_16 - mkl-include 2024.2.2 ha957f24_16 - arch: x86_64 - platform: linux license: LicenseRef-IntelSimplifiedSoftwareOct2022 license_family: Proprietary size: 35857 @@ -13635,8 +12766,6 @@ packages: depends: - mkl 2024.2.2 h66d3029_15 - mkl-include 2024.2.2 h66d3029_15 - arch: x86_64 - platform: win license: LicenseRef-IntelSimplifiedSoftwareOct2022 license_family: Proprietary size: 5299502 @@ -13644,8 +12773,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/mkl-include-2024.2.2-ha957f24_16.conda sha256: 4b72b3acd46c69a8fb56d9f5d8240da811820a18be40765df6e2bd8ea859fbc7 md5: 42b0d14354b5910a9f41e29289914f6b - arch: x86_64 - platform: linux license: LicenseRef-IntelSimplifiedSoftwareOct2022 license_family: Proprietary size: 716756 @@ -13653,8 +12780,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-include-2024.2.2-h66d3029_15.conda sha256: 87b53fd205282de67ff0627ea43d1d4293b7f5d6c5d5a62902c07b71bee7eb58 md5: e2f516189b44b6e042199d13e7015361 - arch: x86_64 - platform: win license: LicenseRef-IntelSimplifiedSoftwareOct2022 license_family: Proprietary size: 718823 @@ -13668,8 +12793,6 @@ packages: - libstdcxx >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 105271 @@ -13683,8 +12806,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 90793 @@ -13698,8 +12819,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 88169 @@ -13707,8 +12826,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2 sha256: 99358d58d778abee4dca82ad29fb58058571f19b0f86138363c260049d4ac7f1 md5: b0309b72560df66f71a9d5e34a5efdfa - arch: x86_64 - platform: win size: 3227 timestamp: 1608166968312 - conda: https://conda.anaconda.org/conda-forge/noarch/multipledispatch-0.6.0-pyhd8ed1ab_1.conda @@ -13742,8 +12859,6 @@ packages: - python_abi 3.10.* *_cp310 - tomli >=1.1.0 - typing_extensions >=4.1.0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 18157960 @@ -13759,8 +12874,6 @@ packages: - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - typing_extensions >=4.1.0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 18730461 @@ -13776,8 +12889,6 @@ packages: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - typing_extensions >=4.1.0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 18996006 @@ -13793,8 +12904,6 @@ packages: - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - typing_extensions >=4.1.0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 17329964 @@ -13811,8 +12920,6 @@ packages: - python_abi 3.9.* *_cp39 - tomli >=1.1.0 - typing_extensions >=4.1.0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 18102742 @@ -13829,8 +12936,6 @@ packages: - python_abi 3.10.* *_cp310 - tomli >=1.1.0 - typing_extensions >=4.1.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 9526636 @@ -13846,8 +12951,6 @@ packages: - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - typing_extensions >=4.1.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 10180870 @@ -13863,8 +12966,6 @@ packages: - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - typing_extensions >=4.1.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 10055116 @@ -13880,8 +12981,6 @@ packages: - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - typing_extensions >=4.1.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 10132728 @@ -13898,8 +12997,6 @@ packages: - python_abi 3.9.* *_cp39 - tomli >=1.1.0 - typing_extensions >=4.1.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 9476213 @@ -13917,8 +13014,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 9904057 @@ -13935,8 +13030,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 10553025 @@ -13953,8 +13046,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 10339284 @@ -13971,8 +13062,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 8735938 @@ -13990,8 +13079,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 9780799 @@ -14122,8 +13209,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc-ng >=12 - arch: x86_64 - platform: linux license: X11 AND BSD-3-Clause size: 889086 timestamp: 1724658547447 @@ -14132,8 +13217,6 @@ packages: md5: cb2b0ea909b97b3d70cd3921d1445e1a depends: - __osx >=11.0 - arch: arm64 - platform: osx license: X11 AND BSD-3-Clause size: 802321 timestamp: 1724658775723 @@ -14209,8 +13292,6 @@ packages: - scipy >=1.0 - libopenblas !=0.3.6 - cudatoolkit >=11.2 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 5695278 @@ -14236,8 +13317,6 @@ packages: - cuda-version >=11.2 - libopenblas >=0.3.18, !=0.3.20 - scipy >=1.0 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 5653160 @@ -14261,8 +13340,6 @@ packages: - scipy >=1.0 - cuda-version >=11.2 - cudatoolkit >=11.2 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 5677692 @@ -14277,8 +13354,6 @@ packages: - numpy >=1.19,<3 - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 134873 @@ -14296,8 +13371,6 @@ packages: - numpy >=1.23.0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 198143 @@ -14314,8 +13387,6 @@ packages: - numpy >=1.23.0 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 173175 @@ -14332,8 +13403,6 @@ packages: - numpy >=1.23.0 - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 200575 @@ -14350,8 +13419,6 @@ packages: - numpy >=1.23.0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 196064 @@ -14368,8 +13435,6 @@ packages: - numpy >=1.23.0 - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 195919 @@ -14386,8 +13451,6 @@ packages: - numpy >=1.23.0 - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 172562 @@ -14402,8 +13465,6 @@ packages: - python >=3.9,<3.10.0a0 - python >=3.9,<3.10.0a0 *_cpython - python_abi 3.9.* *_cp39 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 118431 @@ -14419,8 +13480,6 @@ packages: - python >=3.10,<3.11.0a0 - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 161288 @@ -14436,8 +13495,6 @@ packages: - python >=3.11,<3.12.0a0 - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 187892 @@ -14453,8 +13510,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 184003 @@ -14470,8 +13525,6 @@ packages: - python >=3.13,<3.14.0a0 - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 185097 @@ -14487,8 +13540,6 @@ packages: - python >=3.9,<3.10.0a0 - python >=3.9,<3.10.0a0 *_cpython - python_abi 3.9.* *_cp39 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 160650 @@ -14504,8 +13555,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 127672 @@ -14523,8 +13572,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 189669 @@ -14541,8 +13588,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 165875 @@ -14559,8 +13604,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 192407 @@ -14577,8 +13620,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 188586 @@ -14595,8 +13636,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 190073 @@ -14613,8 +13652,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 165613 @@ -14632,8 +13669,6 @@ packages: - python_abi 3.9.* *_cp39 constrains: - numpy-base <0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 7145497 @@ -14651,8 +13686,6 @@ packages: - python_abi 3.12.* *_cp312 constrains: - numpy-base <0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 7484186 @@ -14671,8 +13704,6 @@ packages: - python_abi 3.9.* *_cp39 constrains: - numpy-base <0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 7925462 @@ -14691,8 +13722,6 @@ packages: - python_abi 3.10.* *_cp310 constrains: - numpy-base <0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 7912254 @@ -14711,8 +13740,6 @@ packages: - python_abi 3.11.* *_cp311 constrains: - numpy-base <0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 9014710 @@ -14731,8 +13758,6 @@ packages: - python_abi 3.12.* *_cp312 constrains: - numpy-base <0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 8483009 @@ -14751,8 +13776,6 @@ packages: - python_abi 3.13.* *_cp313 constrains: - numpy-base <0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 8478406 @@ -14770,8 +13793,6 @@ packages: - python_abi 3.9.* *_cp39 constrains: - numpy-base <0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 5998159 @@ -14789,8 +13810,6 @@ packages: - python_abi 3.12.* *_cp312 constrains: - numpy-base <0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 6073136 @@ -14809,8 +13828,6 @@ packages: - python_abi 3.9.* *_cp39 constrains: - numpy-base <0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 5796232 @@ -14829,8 +13846,6 @@ packages: - python_abi 3.10.* *_cp310 constrains: - numpy-base <0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 5929029 @@ -14849,8 +13864,6 @@ packages: - python_abi 3.11.* *_cp311 constrains: - numpy-base <0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 7067777 @@ -14869,8 +13882,6 @@ packages: - python_abi 3.12.* *_cp312 constrains: - numpy-base <0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 6495249 @@ -14889,8 +13900,6 @@ packages: - python_abi 3.13.* *_cp313 constrains: - numpy-base <0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 6513050 @@ -14908,8 +13917,6 @@ packages: - vs2015_runtime >=14.16.27033 constrains: - numpy-base <0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6419736 @@ -14928,8 +13935,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - numpy-base <0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6495445 @@ -14948,8 +13953,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - numpy-base <0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6325759 @@ -14968,8 +13971,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - numpy-base <0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6420026 @@ -14988,8 +13989,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - numpy-base <0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 7682709 @@ -15008,8 +14007,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - numpy-base <0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 7150899 @@ -15028,8 +14025,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - numpy-base <0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 7147174 @@ -15059,8 +14054,6 @@ packages: - xorg-libxrender >=0.9.11,<0.10.0a0 - xorg-libxt >=1.3.1,<2.0a0 - xorg-libxtst >=1.2.5,<2.0a0 - arch: x86_64 - platform: linux license: GPL-2.0-or-later WITH Classpath-exception-2.0 license_family: GPL size: 190206457 @@ -15070,8 +14063,6 @@ packages: md5: 70c606ff359321d5dc04249dcfdb7944 depends: - libzlib >=1.3.1,<2.0a0 - arch: arm64 - platform: osx license: GPL-2.0-or-later WITH Classpath-exception-2.0 license_family: GPL size: 183318689 @@ -15082,8 +14073,6 @@ packages: depends: - symlink-exe-runtime >=1.0,<2.0a0 - vc14_runtime - arch: x86_64 - platform: win license: GPL-2.0-or-later WITH Classpath-exception-2.0 license_family: GPL size: 183359651 @@ -15098,8 +14087,6 @@ packages: - libstdcxx >=13 - libtiff >=4.7.0,<4.8.0a0 - libzlib >=1.3.1,<2.0a0 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 342988 @@ -15113,8 +14100,6 @@ packages: - libpng >=1.6.44,<1.7.0a0 - libtiff >=4.7.0,<4.8.0a0 - libzlib >=1.3.1,<2.0a0 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 319362 @@ -15129,8 +14114,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 240148 @@ -15161,8 +14144,6 @@ packages: - __glibc >=2.17,<3.0.a0 - ca-certificates - libgcc >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 2937158 @@ -15173,8 +14154,6 @@ packages: depends: - __osx >=11.0 - ca-certificates - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 2936415 @@ -15187,8 +14166,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 8462960 @@ -15206,8 +14183,6 @@ packages: - snappy >=1.2.1,<1.3.0a0 - tzdata - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 1188881 @@ -15224,8 +14199,6 @@ packages: - snappy >=1.2.1,<1.3.0a0 - tzdata - zstd >=1.5.6,<1.6.0a0 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 438520 @@ -15243,8 +14216,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 902551 @@ -15280,8 +14251,6 @@ packages: - python_abi 3.9.* *_cp39 - pytz >=2017.2 - setuptools <60.0.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 12649327 @@ -15300,8 +14269,6 @@ packages: - python-tzdata >=2022a - python_abi 3.10.* *_cp310 - pytz >=2020.1,<2024.2 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 13014228 @@ -15320,8 +14287,6 @@ packages: - python-tzdata >=2022a - python_abi 3.11.* *_cp311 - pytz >=2020.1,<2024.2 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 15695466 @@ -15340,8 +14305,6 @@ packages: - python-tzdata >=2022a - python_abi 3.12.* *_cp312 - pytz >=2020.1,<2024.2 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 15436913 @@ -15360,8 +14323,6 @@ packages: - python-tzdata >=2022a - python_abi 3.13.* *_cp313 - pytz >=2020.1,<2024.2 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 15407410 @@ -15380,8 +14341,6 @@ packages: - python-tzdata >=2022a - python_abi 3.9.* *_cp39 - pytz >=2020.1,<2024.2 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 12914056 @@ -15397,8 +14356,6 @@ packages: - python_abi 3.9.* *_cp39 - pytz >=2017.2 - setuptools <60.0.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 11135664 @@ -15417,8 +14374,6 @@ packages: - python-tzdata >=2022a - python_abi 3.10.* *_cp310 - pytz >=2020.1,<2024.2 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 12024352 @@ -15437,8 +14392,6 @@ packages: - python-tzdata >=2022a - python_abi 3.11.* *_cp311 - pytz >=2020.1,<2024.2 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 14807397 @@ -15457,8 +14410,6 @@ packages: - python-tzdata >=2022a - python_abi 3.12.* *_cp312 - pytz >=2020.1,<2024.2 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 14470437 @@ -15477,8 +14428,6 @@ packages: - python-tzdata >=2022a - python_abi 3.13.* *_cp313 - pytz >=2020.1,<2024.2 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 14464446 @@ -15497,8 +14446,6 @@ packages: - python-tzdata >=2022a - python_abi 3.9.* *_cp39 - pytz >=2020.1,<2024.2 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 12034805 @@ -15515,8 +14462,6 @@ packages: - vc >=14.1,<15.0a0 - vs2015_runtime >=14.16.27012 - setuptools <60.0.0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 10696174 @@ -15535,8 +14480,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 11810567 @@ -15555,8 +14498,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 14587131 @@ -15575,8 +14516,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 14218658 @@ -15595,8 +14534,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 14215159 @@ -15615,8 +14552,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 11755072 @@ -15624,8 +14559,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/pandoc-3.6.1-ha770c72_0.conda sha256: 08092ddd880a58c75feaf37a374826d004c9dacd2cc73bbb623ad4e0bb321b82 md5: e94dd7479ba12963364d855fb23cce4f - arch: x86_64 - platform: linux license: GPL-2.0-or-later license_family: GPL size: 21292558 @@ -15633,8 +14566,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandoc-3.6.1-hce30654_0.conda sha256: 5b33404556b77f479785fc1c77bc0296f560e87f3b6817f48c121e93ebb35ad1 md5: bb356d00e8bbaf3e546f75a30c230ade - arch: arm64 - platform: osx license: GPL-2.0-or-later license_family: GPL size: 23199653 @@ -15642,8 +14573,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/win-64/pandoc-3.6.1-h57928b3_0.conda sha256: 5015bea3441ae0e919960633f55fb5f7dee757a6f893f2be2065001e8e8b18c0 md5: 5d2bd384ec8ed9f60454e1260eb08220 - arch: x86_64 - platform: win license: GPL-2.0-or-later license_family: GPL size: 25266889 @@ -15705,8 +14634,6 @@ packages: - bzip2 >=1.0.8,<2.0a0 - libgcc-ng >=12 - libzlib >=1.3.1,<2.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 952308 @@ -15718,8 +14645,6 @@ packages: - __osx >=11.0 - bzip2 >=1.0.8,<2.0a0 - libzlib >=1.3.1,<2.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 618973 @@ -15733,8 +14658,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 820831 @@ -15774,8 +14697,6 @@ packages: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - tk >=8.6.13,<8.7.0a0 - arch: x86_64 - platform: linux license: HPND size: 42749785 timestamp: 1735929845390 @@ -15796,8 +14717,6 @@ packages: - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - tk >=8.6.13,<8.7.0a0 - arch: arm64 - platform: osx license: HPND size: 42852329 timestamp: 1735930118976 @@ -15819,8 +14738,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: HPND size: 41878282 timestamp: 1735930321933 @@ -15851,8 +14768,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 381072 @@ -15897,6 +14812,17 @@ packages: license_family: MIT size: 193591 timestamp: 1734267205422 +- conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-hooks-5.0.0-pyhd8ed1ab_2.conda + sha256: b3c0e650280e660268c5c3a609c1d008fab598c41eb310f5c6993590889625e7 + md5: f41a1e00c55bc911fcc9cab2a88b4a66 + depends: + - python >=3.9 + - ruamel.yaml >=0.15 + - tomli >=1.1.0 + license: MIT + license_family: MIT + size: 34986 + timestamp: 1734603755600 - conda: https://conda.anaconda.org/conda-forge/linux-64/proj-9.5.1-h0054346_0.conda sha256: 835afb9c8198895ec1ce2916320503d47bb0c25b75c228d744c44e505f1f4e3b md5: 398cabfd9bd75e90d0901db95224f25f @@ -15910,8 +14836,6 @@ packages: - sqlite constrains: - proj4 ==999999999999 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 3108751 @@ -15928,8 +14852,6 @@ packages: - sqlite constrains: - proj4 ==999999999999 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 2673401 @@ -15947,8 +14869,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - proj4 ==999999999999 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 2740461 @@ -15982,8 +14902,6 @@ packages: - libgcc >=13 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 368620 @@ -15996,8 +14914,6 @@ packages: - libgcc >=13 - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 505002 @@ -16010,8 +14926,6 @@ packages: - libgcc >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 487053 @@ -16024,8 +14938,6 @@ packages: - libgcc >=13 - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 495006 @@ -16038,8 +14950,6 @@ packages: - libgcc >=13 - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 364359 @@ -16052,8 +14962,6 @@ packages: - python >=3.10,<3.11.0a0 - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 376278 @@ -16066,8 +14974,6 @@ packages: - python >=3.11,<3.12.0a0 - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 513693 @@ -16080,8 +14986,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 495397 @@ -16094,8 +14998,6 @@ packages: - python >=3.13,<3.14.0a0 - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 502858 @@ -16108,8 +15010,6 @@ packages: - python >=3.9,<3.10.0a0 - python >=3.9,<3.10.0a0 *_cpython - python_abi 3.9.* *_cp39 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 373376 @@ -16123,8 +15023,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 386297 @@ -16138,8 +15036,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 521233 @@ -16153,8 +15049,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 504977 @@ -16168,8 +15062,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 511743 @@ -16183,8 +15075,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 381358 @@ -16195,8 +15085,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 8252 @@ -16206,8 +15094,6 @@ packages: md5: 415816daf82e0b23a736a069a75e9da7 depends: - __osx >=11.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 8381 @@ -16219,8 +15105,6 @@ packages: - libgcc >=13 - libwinpthread >=12.0.0.r4.gg4f2fc60ca - ucrt >=10.0.20348.0 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 9389 @@ -16253,8 +15137,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 25169 @@ -16270,8 +15152,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 25199 @@ -16287,8 +15167,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 25213 @@ -16304,8 +15182,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 25179 @@ -16321,8 +15197,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 25150 @@ -16338,8 +15212,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 25411 @@ -16355,8 +15227,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 25322 @@ -16372,8 +15242,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 25375 @@ -16389,8 +15257,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 25382 @@ -16406,8 +15272,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 25295 @@ -16423,8 +15287,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 25680 @@ -16440,8 +15302,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 25662 @@ -16457,8 +15317,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 25624 @@ -16474,8 +15332,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 25682 @@ -16491,8 +15347,6 @@ packages: - pyarrow-core 18.1.0 *_0_* - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 25640 @@ -16511,8 +15365,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 4544985 @@ -16531,8 +15383,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 4562010 @@ -16551,8 +15401,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 4612916 @@ -16571,8 +15419,6 @@ packages: constrains: - apache-arrow-proc =*=cpu - numpy >=1.21,<3 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 4591002 @@ -16591,8 +15437,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 4531509 @@ -16611,8 +15455,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 3881631 @@ -16631,8 +15473,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 3974075 @@ -16651,8 +15491,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 3909116 @@ -16671,8 +15509,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 3904587 @@ -16691,8 +15527,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: arm64 - platform: osx license: Apache-2.0 license_family: APACHE size: 3882841 @@ -16711,8 +15545,6 @@ packages: constrains: - apache-arrow-proc =*=cpu - numpy >=1.21,<3 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 3425932 @@ -16731,8 +15563,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 3498372 @@ -16751,8 +15581,6 @@ packages: constrains: - apache-arrow-proc =*=cpu - numpy >=1.21,<3 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 3416553 @@ -16771,8 +15599,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 3411909 @@ -16791,8 +15617,6 @@ packages: constrains: - numpy >=1.21,<3 - apache-arrow-proc =*=cpu - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 3433446 @@ -16825,8 +15649,6 @@ packages: - libgcc >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 1645627 @@ -16840,8 +15662,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 1629078 @@ -16855,8 +15675,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 1687506 @@ -16880,8 +15698,6 @@ packages: - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - setuptools - arch: arm64 - platform: osx license: MIT license_family: MIT size: 484571 @@ -16896,8 +15712,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 380414 @@ -16914,8 +15728,6 @@ packages: - packaging - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 640043 @@ -16932,8 +15744,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 564110 @@ -16950,8 +15760,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 806696 @@ -16975,8 +15783,6 @@ packages: - proj >=9.5.0,<9.6.0a0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 555468 @@ -16991,8 +15797,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 494511 @@ -17008,8 +15812,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 748941 @@ -17088,8 +15890,6 @@ packages: - tzdata constrains: - python_abi 3.10.* *_cp310 - arch: x86_64 - platform: linux license: Python-2.0 size: 25199631 timestamp: 1733409331823 @@ -17117,8 +15917,6 @@ packages: - tzdata constrains: - python_abi 3.11.* *_cp311 - arch: x86_64 - platform: linux license: Python-2.0 size: 30624804 timestamp: 1733409665928 @@ -17146,8 +15944,6 @@ packages: - tzdata constrains: - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: Python-2.0 size: 31565686 timestamp: 1733410597922 @@ -17173,8 +15969,6 @@ packages: - readline >=8.2,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata - arch: x86_64 - platform: linux license: Python-2.0 size: 33263183 timestamp: 1733436074842 @@ -17201,8 +15995,6 @@ packages: - tzdata constrains: - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: linux license: Python-2.0 size: 23622848 timestamp: 1733407924273 @@ -17224,8 +16016,6 @@ packages: - tzdata constrains: - python_abi 3.10.* *_cp310 - arch: arm64 - platform: osx license: Python-2.0 size: 12372048 timestamp: 1733408850559 @@ -17248,8 +16038,6 @@ packages: - tzdata constrains: - python_abi 3.11.* *_cp311 - arch: arm64 - platform: osx license: Python-2.0 size: 14647146 timestamp: 1733409012105 @@ -17272,8 +16060,6 @@ packages: - tzdata constrains: - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: Python-2.0 size: 12998673 timestamp: 1733408900971 @@ -17296,8 +16082,6 @@ packages: - readline >=8.2,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata - arch: arm64 - platform: osx license: Python-2.0 size: 12905237 timestamp: 1733433280639 @@ -17319,8 +16103,6 @@ packages: - tzdata constrains: - python_abi 3.9.* *_cp39 - arch: arm64 - platform: osx license: Python-2.0 size: 11800492 timestamp: 1733406732542 @@ -17342,8 +16124,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - python_abi 3.10.* *_cp310 - arch: x86_64 - platform: win license: Python-2.0 size: 16061214 timestamp: 1733408154785 @@ -17366,8 +16146,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - python_abi 3.11.* *_cp311 - arch: x86_64 - platform: win license: Python-2.0 size: 18161635 timestamp: 1733408064601 @@ -17390,8 +16168,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: win license: Python-2.0 size: 15812363 timestamp: 1733408080064 @@ -17414,8 +16190,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Python-2.0 size: 16753813 timestamp: 1733433028707 @@ -17437,8 +16211,6 @@ packages: - vc14_runtime >=14.29.30139 constrains: - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: win license: Python-2.0 size: 16943409 timestamp: 1733406595694 @@ -17485,8 +16257,6 @@ packages: md5: 2921c34715e74b3587b4cff4d36844f9 constrains: - python 3.10.* *_cpython - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 6227 @@ -17497,8 +16267,6 @@ packages: md5: 139a8d40c8a2f430df31048949e450de constrains: - python 3.11.* *_cpython - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 6211 @@ -17509,8 +16277,6 @@ packages: md5: 0424ae29b104430108f5218a66db7260 constrains: - python 3.12.* *_cpython - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 6238 @@ -17521,8 +16287,6 @@ packages: md5: 381bbd2a92c863f640a55b6ff3c35161 constrains: - python 3.13.* *_cp313 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 6217 @@ -17533,8 +16297,6 @@ packages: md5: 40363a30db350596b5f225d0d5a33328 constrains: - python 3.9.* *_cpython - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 6193 @@ -17545,8 +16307,6 @@ packages: md5: e33836c9096802b29d28981765becbee constrains: - python 3.10.* *_cpython - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 6324 @@ -17557,8 +16317,6 @@ packages: md5: 3b855e3734344134cb56c410f729c340 constrains: - python 3.11.* *_cpython - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 6308 @@ -17569,8 +16327,6 @@ packages: md5: b76f9b1c862128e56ac7aa8cd2333de9 constrains: - python 3.12.* *_cpython - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 6278 @@ -17581,8 +16337,6 @@ packages: md5: b8e82d0a5c1664638f87f63cc5d241fb constrains: - python 3.13.* *_cp313 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 6322 @@ -17593,8 +16347,6 @@ packages: md5: 1ca4a5e8290873da8963182d9673299d constrains: - python 3.9.* *_cpython - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 6326 @@ -17605,8 +16357,6 @@ packages: md5: 3c510f4c4383f5fbdb12fdd971b30d49 constrains: - python 3.10.* *_cpython - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6715 @@ -17617,8 +16367,6 @@ packages: md5: 895b873644c11ccc0ab7dba2d8513ae6 constrains: - python 3.11.* *_cpython - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6707 @@ -17629,8 +16377,6 @@ packages: md5: e8681f534453af7afab4cd2bc1423eec constrains: - python 3.12.* *_cpython - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6730 @@ -17641,8 +16387,6 @@ packages: md5: 44b4fe6f22b57103afb2299935c8b68e constrains: - python 3.13.* *_cp313 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6716 @@ -17653,8 +16397,6 @@ packages: md5: 86ba1bbcf9b259d1592201f3c345c810 constrains: - python 3.9.* *_cpython - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6706 @@ -17686,8 +16428,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: PSF-2.0 license_family: PSF size: 6032183 @@ -17701,8 +16441,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: PSF-2.0 license_family: PSF size: 6060096 @@ -17717,8 +16455,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - winpty - arch: x86_64 - platform: win license: MIT license_family: MIT size: 210034 @@ -17732,8 +16468,6 @@ packages: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - yaml >=0.2.5,<0.3.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 206553 @@ -17747,8 +16481,6 @@ packages: - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - yaml >=0.2.5,<0.3.0a0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 187143 @@ -17762,8 +16494,6 @@ packages: - python >=3.13.0rc1,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - yaml >=0.2.5,<0.3.0a0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 187550 @@ -17778,8 +16508,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - yaml >=0.2.5,<0.3.0a0 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 181227 @@ -17794,8 +16522,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - yaml >=0.2.5,<0.3.0a0 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 181722 @@ -17811,8 +16537,6 @@ packages: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - zeromq >=4.3.5,<4.4.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 378126 @@ -17828,8 +16552,6 @@ packages: - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - zeromq >=4.3.5,<4.4.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 361674 @@ -17845,8 +16567,6 @@ packages: - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - zeromq >=4.3.5,<4.4.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 365164 @@ -17862,8 +16582,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - zeromq >=4.3.5,<4.3.6.0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 360217 @@ -17879,8 +16597,6 @@ packages: - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - zeromq >=4.3.5,<4.3.6.0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 367463 @@ -17892,8 +16608,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc-ng >=12 - libstdcxx-ng >=12 - arch: x86_64 - platform: linux license: LicenseRef-Qhull size: 552937 timestamp: 1720813982144 @@ -17903,8 +16617,6 @@ packages: depends: - __osx >=11.0 - libcxx >=16 - arch: arm64 - platform: osx license: LicenseRef-Qhull size: 516376 timestamp: 1720814307311 @@ -17915,8 +16627,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: LicenseRef-Qhull size: 1377020 timestamp: 1720814433486 @@ -17925,8 +16635,6 @@ packages: md5: 77d9955b4abddb811cb8ab1aa7d743e4 depends: - libgcc-ng >=12 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 15423721 @@ -17934,8 +16642,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rav1e-0.6.6-h69fbcac_2.conda sha256: be6174970193cb4d0ffa7d731a93a4c9542881dbc7ab24e74b460ef312161169 md5: e309ae86569b1cd55a0285fa4e939844 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 1526706 @@ -17947,8 +16653,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 1523119 @@ -17958,8 +16662,6 @@ packages: md5: e84ddf12bde691e8ec894b00ea829ddf depends: - libre2-11 2024.07.02 hbbce691_2 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 26786 @@ -17969,8 +16671,6 @@ packages: md5: 7a8b4ad8c58a3408ca89d78788c78178 depends: - libre2-11 2024.07.02 h07bc746_2 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 26861 @@ -17980,8 +16680,6 @@ packages: md5: 10980cbe103147435a40288db9f49847 depends: - libre2-11 2024.07.02 h4eb7d71_2 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 214916 @@ -17992,8 +16690,6 @@ packages: depends: - libgcc-ng >=12 - ncurses >=6.3,<7.0a0 - arch: x86_64 - platform: linux license: GPL-3.0-only license_family: GPL size: 281456 @@ -18003,8 +16699,6 @@ packages: md5: 8cbb776a2f641b943d413b3e19df71f4 depends: - ncurses >=6.3,<7.0a0 - arch: arm64 - platform: osx license: GPL-3.0-only license_family: GPL size: 250351 @@ -18064,8 +16758,6 @@ packages: - python_abi 3.12.* *_cp312 constrains: - __glibc >=2.17 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 354410 @@ -18080,8 +16772,6 @@ packages: - python_abi 3.12.* *_cp312 constrains: - __osx >=11.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 318920 @@ -18095,12 +16785,99 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 225369 timestamp: 1733367159579 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.10-py312h66e93f0_0.conda + sha256: cd8ed10671111f15245cebadc06b88d6f5fc91f1f7f92456daa568e9d9f5bc42 + md5: 5260b7fb19694ee5bc4ed0ee7a2a769f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ruamel.yaml.clib >=0.1.2 + arch: x86_64 + platform: linux + license: MIT + license_family: MIT + size: 267560 + timestamp: 1736248154294 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml-0.18.10-py313h90d716c_0.conda + sha256: d996e7c5ba298bef243c712e5ea96ba8ccbb5000b9d0eb9382fb6ccc2098dc08 + md5: c7b68850c7ac1beeff88404d8b77d93b + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + - ruamel.yaml.clib >=0.1.2 + arch: arm64 + platform: osx + license: MIT + license_family: MIT + size: 269937 + timestamp: 1736248226069 +- conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml-0.18.10-py313ha7868ed_0.conda + sha256: 8c7e07449ab94e7e6b218357bae1670401c20ac99a27c2970ced83d101774020 + md5: c81330e1131129da89af4aaf6d8cd03a + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ruamel.yaml.clib >=0.1.2 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + arch: x86_64 + platform: win + license: MIT + license_family: MIT + size: 269261 + timestamp: 1736248205677 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.8-py312h66e93f0_1.conda + sha256: ac987b1c186d79e4e1ce4354a84724fc68db452b2bd61de3a3e1b6fc7c26138d + md5: 532c3e5d0280be4fea52396ec1fa7d5d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + arch: x86_64 + platform: linux + license: MIT + license_family: MIT + size: 145481 + timestamp: 1728724626666 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml.clib-0.2.8-py313h63a2874_1.conda + sha256: 8ed7448178b423dbd59cdea422b1fb732c16beacff2cc70f727eff1afd307896 + md5: 34ad7f96e9e4bae5f9a88d0fb04ad557 + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + arch: arm64 + platform: osx + license: MIT + license_family: MIT + size: 115973 + timestamp: 1728724684349 +- conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml.clib-0.2.8-py313ha7868ed_1.conda + sha256: d462f89d59f73686f324b603cc6fed4db49f7337143ad4447ac9b6fd68610e67 + md5: 86dc53d90ebfb62cbe18217f7f2ddd72 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + arch: x86_64 + platform: win + license: MIT + license_family: MIT + size: 108488 + timestamp: 1728724833760 - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.8.6-py312h2156523_0.conda sha256: 5da6e2380da29453c6fc86bd7fad879fd302785a37c5cb9bda7a311c967d7181 md5: 284282b754028b3271019713669aa23c @@ -18112,8 +16889,6 @@ packages: - python_abi 3.12.* *_cp312 constrains: - __glibc >=2.17 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 7974925 @@ -18129,8 +16904,6 @@ packages: - python_abi 3.13.* *_cp313 constrains: - __osx >=11.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 7025487 @@ -18144,8 +16917,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 6956211 @@ -18157,8 +16928,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - openssl >=3.4.0,<4.0a0 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 355142 @@ -18176,8 +16945,6 @@ packages: - python_abi 3.9.* *_cp39 - scipy - threadpoolctl - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 7934018 @@ -18196,8 +16963,6 @@ packages: - python_abi 3.10.* *_cp310 - scipy - threadpoolctl >=3.1.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 9403228 @@ -18216,8 +16981,6 @@ packages: - python_abi 3.11.* *_cp311 - scipy - threadpoolctl >=3.1.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 10771479 @@ -18236,8 +16999,6 @@ packages: - python_abi 3.12.* *_cp312 - scipy - threadpoolctl >=3.1.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 10537886 @@ -18256,8 +17017,6 @@ packages: - python_abi 3.13.* *_cp313 - scipy - threadpoolctl >=3.1.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 10561945 @@ -18276,8 +17035,6 @@ packages: - python_abi 3.9.* *_cp39 - scipy - threadpoolctl >=3.1.0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 9397931 @@ -18296,8 +17053,6 @@ packages: - python_abi 3.9.* *_cp39 - scipy - threadpoolctl - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 6931407 @@ -18316,8 +17071,6 @@ packages: - python_abi 3.10.* *_cp310 - scipy - threadpoolctl >=3.1.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 8548492 @@ -18336,8 +17089,6 @@ packages: - python_abi 3.11.* *_cp311 - scipy - threadpoolctl >=3.1.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 9842923 @@ -18356,8 +17107,6 @@ packages: - python_abi 3.12.* *_cp312 - scipy - threadpoolctl >=3.1.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 9705901 @@ -18376,8 +17125,6 @@ packages: - python_abi 3.13.* *_cp313 - scipy - threadpoolctl >=3.1.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 9772810 @@ -18396,8 +17143,6 @@ packages: - python_abi 3.9.* *_cp39 - scipy - threadpoolctl >=3.1.0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 8578268 @@ -18415,8 +17160,6 @@ packages: - threadpoolctl - vc >=14.1,<15.0a0 - vs2015_runtime >=14.16.27012 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 6868691 @@ -18434,8 +17177,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 8361113 @@ -18453,8 +17194,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 9649866 @@ -18472,8 +17211,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 9437623 @@ -18491,8 +17228,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 9485378 @@ -18510,8 +17245,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 8256853 @@ -18531,8 +17264,6 @@ packages: - numpy >=1.19,<3 - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 16523290 @@ -18554,8 +17285,6 @@ packages: - numpy >=1.23.5 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 18405029 @@ -18577,8 +17306,6 @@ packages: - numpy >=1.23.5 - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 19423109 @@ -18600,8 +17327,6 @@ packages: - numpy >=1.23.5 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 18664052 @@ -18623,8 +17348,6 @@ packages: - numpy >=1.23.5 - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 19188982 @@ -18646,8 +17369,6 @@ packages: - python_abi 3.9.* *_cp39 constrains: - libopenblas <0.3.26 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 24110689 @@ -18669,8 +17390,6 @@ packages: - python >=3.9,<3.10.0a0 - python >=3.9,<3.10.0a0 *_cpython - python_abi 3.9.* *_cp39 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 14699719 @@ -18692,8 +17411,6 @@ packages: - python >=3.10,<3.11.0a0 - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 15057534 @@ -18715,8 +17432,6 @@ packages: - python >=3.11,<3.12.0a0 - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 16177424 @@ -18738,8 +17453,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 16060914 @@ -18761,8 +17474,6 @@ packages: - python >=3.13,<3.14.0a0 - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 16242568 @@ -18784,8 +17495,6 @@ packages: - python_abi 3.9.* *_cp39 constrains: - libopenblas <0.3.26 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 19944955 @@ -18804,8 +17513,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 14854560 @@ -18825,8 +17532,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 17162281 @@ -18846,8 +17551,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 18138328 @@ -18867,8 +17570,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 17942085 @@ -18888,8 +17589,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 18006929 @@ -18911,8 +17610,6 @@ packages: - vs2015_runtime >=14.29.30139 constrains: - libopenblas <0.3.26 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 25290512 @@ -18955,8 +17652,6 @@ packages: depends: - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 1047880 @@ -18977,8 +17672,6 @@ packages: - python >=3.9,<3.10.0a0 - python >=3.9,<3.10.0a0 *_cpython - python_abi 3.9.* *_cp39 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 1042646 @@ -18989,8 +17682,6 @@ packages: depends: - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 1046653 @@ -19018,8 +17709,6 @@ packages: - numpy >=1.19,<3 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 571386 @@ -19034,8 +17723,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 532284 @@ -19051,8 +17738,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 534759 @@ -19062,8 +17747,6 @@ packages: md5: 4a2cac04f86a4540b8c9b8d8f597848f depends: - openssl >=3.0.0,<4.0a0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 210264 @@ -19084,8 +17767,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 42739 @@ -19096,8 +17777,6 @@ packages: depends: - __osx >=11.0 - libcxx >=18 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 35857 @@ -19109,8 +17788,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 59757 @@ -19304,8 +17981,6 @@ packages: - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - readline >=8.2,<9.0a0 - arch: x86_64 - platform: linux license: Unlicense size: 884362 timestamp: 1733761834904 @@ -19318,8 +17993,6 @@ packages: - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - readline >=8.2,<9.0a0 - arch: arm64 - platform: osx license: Unlicense size: 853604 timestamp: 1733762084934 @@ -19331,8 +18004,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Unlicense size: 915915 timestamp: 1733762142683 @@ -19360,8 +18031,6 @@ packages: - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - scipy >=1.4,!=1.9.2 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 10945332 @@ -19380,8 +18049,6 @@ packages: - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - scipy !=1.9.2,>=1.8 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 10864201 @@ -19400,8 +18067,6 @@ packages: - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - scipy !=1.9.2,>=1.8 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 12291537 @@ -19420,8 +18085,6 @@ packages: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - scipy !=1.9.2,>=1.8 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 12103203 @@ -19440,8 +18103,6 @@ packages: - python >=3.13.0rc2,<3.14.0a0 - python_abi 3.13.* *_cp313 - scipy !=1.9.2,>=1.8 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 12272855 @@ -19460,8 +18121,6 @@ packages: - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - scipy !=1.9.2,>=1.8 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 10853209 @@ -19478,8 +18137,6 @@ packages: - python >=3.9,<3.10.0a0 *_cpython - python_abi 3.9.* *_cp39 - scipy >=1.4,!=1.9.2 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 10477859 @@ -19498,8 +18155,6 @@ packages: - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 - scipy !=1.9.2,>=1.8 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 10461482 @@ -19518,8 +18173,6 @@ packages: - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - scipy !=1.9.2,>=1.8 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 11976331 @@ -19538,8 +18191,6 @@ packages: - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - scipy !=1.9.2,>=1.8 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 11832944 @@ -19558,8 +18209,6 @@ packages: - python >=3.13.0rc2,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - scipy !=1.9.2,>=1.8 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 11901433 @@ -19578,8 +18227,6 @@ packages: - python >=3.9,<3.10.0a0 *_cpython - python_abi 3.9.* *_cp39 - scipy !=1.9.2,>=1.8 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 10489122 @@ -19598,8 +18245,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 10380509 @@ -19619,8 +18264,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 10374616 @@ -19640,8 +18283,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 11707520 @@ -19661,8 +18302,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 11693380 @@ -19682,8 +18321,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 11727420 @@ -19703,8 +18340,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 10323515 @@ -19716,8 +18351,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libstdcxx >=13 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 2746291 @@ -19728,8 +18361,6 @@ packages: depends: - __osx >=11.0 - libcxx >=17 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 1387330 @@ -19741,8 +18372,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 1845727 @@ -19754,8 +18383,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vs2015_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 11597 @@ -19785,8 +18412,6 @@ packages: - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - scipy - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 619425 @@ -19807,8 +18432,6 @@ packages: - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - scipy - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 620338 @@ -19829,8 +18452,6 @@ packages: - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - scipy - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 646735 @@ -19851,8 +18472,6 @@ packages: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - scipy - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 628827 @@ -19873,8 +18492,6 @@ packages: - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - scipy - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 637288 @@ -19895,8 +18512,6 @@ packages: - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - scipy - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 621937 @@ -19916,8 +18531,6 @@ packages: - python >=3.9,<3.10.0a0 *_cpython - python_abi 3.9.* *_cp39 - scipy - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 498503 @@ -19938,8 +18551,6 @@ packages: - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 - scipy - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 502389 @@ -19960,8 +18571,6 @@ packages: - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - scipy - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 516869 @@ -19982,8 +18591,6 @@ packages: - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - scipy - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 521155 @@ -20004,8 +18611,6 @@ packages: - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - scipy - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 530635 @@ -20026,8 +18631,6 @@ packages: - python >=3.9,<3.10.0a0 *_cpython - python_abi 3.9.* *_cp39 - scipy - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 503501 @@ -20045,8 +18648,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 439389 @@ -20065,8 +18666,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 441028 @@ -20085,8 +18684,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 472146 @@ -20105,8 +18702,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 465327 @@ -20125,8 +18720,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 463009 @@ -20145,8 +18738,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 442922 @@ -20167,8 +18758,6 @@ packages: - __osx >=11.0 - libcxx >=17.0.0.a0 - ncurses >=6.5,<7.0a0 - arch: arm64 - platform: osx license: NCSA license_family: MIT size: 207679 @@ -20181,8 +18770,6 @@ packages: - libgcc >=13 - libhwloc >=2.11.2,<2.11.3.0a0 - libstdcxx >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: APACHE size: 175954 @@ -20195,8 +18782,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: APACHE size: 151460 @@ -20271,8 +18856,6 @@ packages: depends: - libgcc-ng >=12 - libzlib >=1.2.13,<2.0.0a0 - arch: x86_64 - platform: linux license: TCL license_family: BSD size: 3318875 @@ -20282,8 +18865,6 @@ packages: md5: b50a57ba89c32b62428b71a875291c9b depends: - libzlib >=1.2.13,<2.0.0a0 - arch: arm64 - platform: osx license: TCL license_family: BSD size: 3145523 @@ -20295,8 +18876,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: TCL license_family: BSD size: 3503410 @@ -20345,8 +18924,6 @@ packages: - libgcc >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 840414 @@ -20359,8 +18936,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 842549 @@ -20373,8 +18948,6 @@ packages: - python >=3.13,<3.14.0a0 - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 863363 @@ -20388,8 +18961,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 844347 @@ -20403,8 +18974,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 865881 @@ -20474,8 +19043,6 @@ packages: md5: 6797b005cd0f439c4c5c9ac565783700 constrains: - vs2015_runtime >=14.29.30037 - arch: x86_64 - platform: win license: LicenseRef-MicrosoftWindowsSDK10 size: 559710 timestamp: 1728377334097 @@ -20489,8 +19056,6 @@ packages: - libstdcxx >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 13904 @@ -20505,8 +19070,6 @@ packages: - python >=3.13.0rc1,<3.14.0a0 - python >=3.13.0rc1,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 13689 @@ -20521,8 +19084,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 17210 @@ -20535,8 +19096,6 @@ packages: - libgcc >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 368550 @@ -20549,8 +19108,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 372492 @@ -20564,8 +19121,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 365482 @@ -20585,8 +19140,6 @@ packages: depends: - libgcc-ng >=12 - libstdcxx-ng >=12 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 48270 @@ -20597,8 +19150,6 @@ packages: depends: - __osx >=11.0 - libcxx >=16 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 40625 @@ -20610,8 +19161,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 49181 @@ -20634,8 +19183,6 @@ packages: md5: 7c10ec3158d1eb4ddff7007c9101adb0 depends: - vc14_runtime >=14.38.33135 - arch: x86_64 - platform: win track_features: - vc14 license: BSD-3-Clause @@ -20649,8 +19196,6 @@ packages: - ucrt >=10.0.20348.0 constrains: - vs2015_runtime 14.42.34433.* *_23 - arch: x86_64 - platform: win license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime license_family: Proprietary size: 754247 @@ -20672,8 +19217,6 @@ packages: md5: 5c176975ca2b8366abad3c97b3cd1e83 depends: - vc14_runtime >=14.42.34433 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 17572 @@ -20685,8 +19228,6 @@ packages: - vswhere constrains: - vs_win-64 2019.11 - arch: x86_64 - platform: win track_features: - vc14 license: BSD-3-Clause @@ -20696,8 +19237,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/win-64/vswhere-3.1.7-h57928b3_0.conda sha256: 8caeda9c0898cb8ee2cf4f45640dbbbdf772ddc01345cfb0f7b352c58b4d8025 md5: ba83df93b48acfc528f5464c9a882baa - arch: x86_64 - platform: win license: MIT license_family: MIT size: 219013 @@ -20770,8 +19309,6 @@ packages: - libgcc >=13 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 56736 @@ -20784,8 +19321,6 @@ packages: - libgcc >=13 - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 65396 @@ -20798,8 +19333,6 @@ packages: - libgcc >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 63807 @@ -20812,8 +19345,6 @@ packages: - libgcc >=13 - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 63871 @@ -20826,8 +19357,6 @@ packages: - libgcc >=13 - python >=3.9,<3.10.0a0 - python_abi 3.9.* *_cp39 - arch: x86_64 - platform: linux license: BSD-2-Clause license_family: BSD size: 56180 @@ -20840,8 +19369,6 @@ packages: - python >=3.10,<3.11.0a0 - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 53892 @@ -20854,8 +19381,6 @@ packages: - python >=3.11,<3.12.0a0 - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 62309 @@ -20868,8 +19393,6 @@ packages: - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 61043 @@ -20882,8 +19405,6 @@ packages: - python >=3.13,<3.14.0a0 - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 61433 @@ -20896,8 +19417,6 @@ packages: - python >=3.9,<3.10.0a0 - python >=3.9,<3.10.0a0 *_cpython - python_abi 3.9.* *_cp39 - arch: arm64 - platform: osx license: BSD-2-Clause license_family: BSD size: 53776 @@ -20911,8 +19430,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 55315 @@ -20926,8 +19443,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 63549 @@ -20941,8 +19456,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 62371 @@ -20956,8 +19469,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 62568 @@ -20971,8 +19482,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-2-Clause license_family: BSD size: 54867 @@ -20983,8 +19492,6 @@ packages: depends: - libgcc-ng >=10.3.0 - libstdcxx-ng >=10.3.0 - arch: x86_64 - platform: linux license: GPL-2.0-or-later license_family: GPL size: 3357188 @@ -20994,8 +19501,6 @@ packages: md5: b1f7f2780feffe310b068c021e8ff9b2 depends: - libcxx >=12.0.1 - arch: arm64 - platform: osx license: GPL-2.0-or-later license_family: GPL size: 1832744 @@ -21006,8 +19511,6 @@ packages: depends: - vc >=14.1,<15 - vs2015_runtime >=14.16.27033 - arch: x86_64 - platform: win license: GPL-2.0-or-later license_family: GPL size: 5517425 @@ -21021,8 +19524,6 @@ packages: - libgcc >=13 - libnsl >=2.0.1,<2.1.0a0 - libstdcxx >=13 - arch: x86_64 - platform: linux license: Apache-2.0 license_family: Apache size: 1648243 @@ -21034,8 +19535,6 @@ packages: - __osx >=11.0 - icu >=75.1,<76.0a0 - libcxx >=17 - arch: arm64 - platform: osx license: Apache-2.0 license_family: Apache size: 1277884 @@ -21047,8 +19546,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Apache-2.0 license_family: Apache size: 3574017 @@ -21068,8 +19565,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 58628 @@ -21082,8 +19577,6 @@ packages: - libgcc >=13 - libuuid >=2.38.1,<3.0a0 - xorg-libice >=1.1.2,<2.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 27198 @@ -21095,8 +19588,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libxcb >=1.17.0,<2.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 837524 @@ -21107,8 +19598,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 14780 @@ -21118,8 +19607,6 @@ packages: md5: 50901e0764b7701d8ed7343496f4f301 depends: - __osx >=11.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 13593 @@ -21131,8 +19618,6 @@ packages: - libgcc >=13 - libwinpthread >=12.0.0.r4.gg4f2fc60ca - ucrt >=10.0.20348.0 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 108013 @@ -21143,8 +19628,6 @@ packages: depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 19901 @@ -21154,8 +19637,6 @@ packages: md5: 77c447f48cab5d3a15ac224edb86a968 depends: - __osx >=11.0 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 18487 @@ -21167,8 +19648,6 @@ packages: - libgcc >=13 - libwinpthread >=12.0.0.r4.gg4f2fc60ca - ucrt >=10.0.20348.0 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 69920 @@ -21180,8 +19659,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - xorg-libx11 >=1.8.10,<2.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 50060 @@ -21193,8 +19670,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - xorg-libx11 >=1.8.10,<2.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 19575 @@ -21208,8 +19683,6 @@ packages: - xorg-libx11 >=1.8.10,<2.0a0 - xorg-libxext >=1.3.6,<2.0a0 - xorg-libxfixes >=6.0.1,<7.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 47179 @@ -21223,8 +19696,6 @@ packages: - xorg-libx11 >=1.8.10,<2.0a0 - xorg-libxext >=1.3.6,<2.0a0 - xorg-libxrender >=0.9.11,<0.10.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 29599 @@ -21236,8 +19707,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - xorg-libx11 >=1.8.10,<2.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 33005 @@ -21251,8 +19720,6 @@ packages: - xorg-libice >=1.1.1,<2.0a0 - xorg-libsm >=1.2.4,<2.0a0 - xorg-libx11 >=1.8.10,<2.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 379686 @@ -21266,8 +19733,6 @@ packages: - xorg-libx11 >=1.8.10,<2.0a0 - xorg-libxext >=1.3.6,<2.0a0 - xorg-libxi >=1.7.10,<2.0a0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 32808 @@ -21278,8 +19743,6 @@ packages: depends: - libgcc-ng >=12 - libstdcxx-ng >=12 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 133694 @@ -21290,8 +19753,6 @@ packages: depends: - __osx >=11.0 - libcxx >=16 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 133777 @@ -21303,8 +19764,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 134447 @@ -21323,8 +19782,6 @@ packages: md5: 4cb3ad778ec2d5a7acbdf254eb1c42ae depends: - libgcc-ng >=9.4.0 - arch: x86_64 - platform: linux license: MIT license_family: MIT size: 89141 @@ -21332,8 +19789,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 sha256: 93181a04ba8cfecfdfb162fc958436d868cc37db504c58078eab4c1a3e57fbb7 md5: 4bb3f014845110883a3c5ee811fd84b4 - arch: arm64 - platform: osx license: MIT license_family: MIT size: 88016 @@ -21344,8 +19799,6 @@ packages: depends: - vc >=14.1,<15.0a0 - vs2015_runtime >=14.16.27012 - arch: x86_64 - platform: win license: MIT license_family: MIT size: 63274 @@ -21359,8 +19812,6 @@ packages: - libgcc >=13 - libsodium >=1.0.20,<1.0.21.0a0 - libstdcxx >=13 - arch: x86_64 - platform: linux license: MPL-2.0 license_family: MOZILLA size: 335400 @@ -21373,8 +19824,6 @@ packages: - krb5 >=1.21.3,<1.22.0a0 - libcxx >=18 - libsodium >=1.0.20,<1.0.21.0a0 - arch: arm64 - platform: osx license: MPL-2.0 license_family: MOZILLA size: 281565 @@ -21388,8 +19837,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: MPL-2.0 license_family: MOZILLA size: 2527503 @@ -21419,8 +19866,6 @@ packages: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libzlib 1.3.1 hb9d3cd8_2 - arch: x86_64 - platform: linux license: Zlib license_family: Other size: 92286 @@ -21431,8 +19876,6 @@ packages: depends: - __osx >=11.0 - libzlib 1.3.1 h8359307_2 - arch: arm64 - platform: osx license: Zlib license_family: Other size: 77606 @@ -21445,8 +19888,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: Zlib license_family: Other size: 107439 @@ -21462,8 +19903,6 @@ packages: - python_abi 3.12.* *_cp312 - zstd >=1.5.6,<1.5.7.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 419552 @@ -21479,8 +19918,6 @@ packages: - python_abi 3.12.* *_cp312 - zstd >=1.5.6,<1.5.7.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 330788 @@ -21496,8 +19933,6 @@ packages: - python_abi 3.13.* *_cp313 - zstd >=1.5.6,<1.5.7.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 336496 @@ -21514,8 +19949,6 @@ packages: - vc14_runtime >=14.29.30139 - zstd >=1.5.6,<1.5.7.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 320624 @@ -21532,8 +19965,6 @@ packages: - vc14_runtime >=14.29.30139 - zstd >=1.5.6,<1.5.7.0a0 - zstd >=1.5.6,<1.6.0a0 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 325703 @@ -21545,8 +19976,6 @@ packages: - libgcc-ng >=12 - libstdcxx-ng >=12 - libzlib >=1.2.13,<2.0.0a0 - arch: x86_64 - platform: linux license: BSD-3-Clause license_family: BSD size: 554846 @@ -21557,8 +19986,6 @@ packages: depends: - __osx >=11.0 - libzlib >=1.2.13,<2.0.0a0 - arch: arm64 - platform: osx license: BSD-3-Clause license_family: BSD size: 405089 @@ -21571,8 +19998,6 @@ packages: - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - arch: x86_64 - platform: win license: BSD-3-Clause license_family: BSD size: 349143 diff --git a/pixi.toml b/pixi.toml index de08f5a70..048d83e72 100644 --- a/pixi.toml +++ b/pixi.toml @@ -91,6 +91,7 @@ memory_profiler = "*" [feature.lint.dependencies] cython-lint = "*" pre-commit = "*" +pre-commit-hooks = "*" ruff = "*" [feature.docs.dependencies] diff --git a/pyproject.toml b/pyproject.toml index ae65043f4..30202b0a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,10 +42,10 @@ check_untyped_defs = true namespace_packages = true ignore_missing_imports = true -[[tool.mypy.overrides]] -# https://github.com/scikit-learn/scikit-learn/issues/16705 -module = ["sklearn.*"] -ignore_missing_imports = true +[[tool.mypy.overrides]] +# https://github.com/scikit-learn/scikit-learn/issues/16705 +module = ["sklearn.*"] +ignore_missing_imports = true [tool.cibuildwheel] skip = [ diff --git a/src/glum/__init__.py b/src/glum/__init__.py index 1006664ed..696966d47 100644 --- a/src/glum/__init__.py +++ b/src/glum/__init__.py @@ -11,8 +11,9 @@ PoissonDistribution, TweedieDistribution, ) -from ._glm import GeneralizedLinearRegressor, get_family, get_link +from ._glm import get_family, get_link from ._glm_cv import GeneralizedLinearRegressorCV +from ._glm_regressor import GeneralizedLinearRegressor from ._link import CloglogLink, IdentityLink, Link, LogitLink, LogLink, TweedieLink try: diff --git a/src/glum/_distribution.py b/src/glum/_distribution.py index 8d794009f..fe6fda823 100644 --- a/src/glum/_distribution.py +++ b/src/glum/_distribution.py @@ -35,8 +35,8 @@ tweedie_log_likelihood, tweedie_log_rowwise_gradient_hessian, ) +from ._linalg import _safe_lin_pred, _safe_sandwich_dot from ._link import IdentityLink, Link, LogitLink, LogLink -from ._util import _safe_lin_pred, _safe_sandwich_dot class ExponentialDispersionModel(metaclass=ABCMeta): @@ -1497,6 +1497,62 @@ def dispersion( # noqa D ) +def get_one_over_variance( + distribution: ExponentialDispersionModel, + link: Link, + mu, + eta, + dispersion: float, + sample_weight, +): + """Get one over the variance. + + For Tweedie: ``sigma_inv = sample_weight / (mu ** p)`` during optimization, + because ``phi = 1``. + + For binomial with logit link: simplifies to + ``variance = phi / ( sample_weight * (exp(eta) + 2 + exp(-eta)))``, + more numerically accurate. + """ + if isinstance(distribution, BinomialDistribution) and isinstance(link, LogitLink): + max_float_for_exp = np.log(np.finfo(eta.dtype).max / 10) + if np.any(np.abs(eta) > max_float_for_exp): + eta = np.clip(eta, -max_float_for_exp, max_float_for_exp) # type: ignore + return sample_weight * (np.exp(eta) + 2 + np.exp(-eta)) / dispersion + return 1.0 / distribution.variance( + mu, dispersion=dispersion, sample_weight=sample_weight + ) + + +def _as_float_arrays(*args): + """Convert to a float array, passing ``None`` through, and broadcast.""" + never_broadcast = {} # type: ignore + maybe_broadcast = {} + always_broadcast = {} + + for ix, arg in enumerate(args): + if isinstance(arg, (int, float)): + maybe_broadcast[ix] = np.array([arg], dtype="float") + elif arg is None: + never_broadcast[ix] = None + else: + always_broadcast[ix] = np.asanyarray(arg, dtype="float") + + if always_broadcast and maybe_broadcast: + to_broadcast = {**always_broadcast, **maybe_broadcast} + _broadcast = np.broadcast_arrays(*to_broadcast.values()) + broadcast = dict(zip(to_broadcast.keys(), _broadcast)) + elif always_broadcast: + _broadcast = np.broadcast_arrays(*always_broadcast.values()) + broadcast = dict(zip(always_broadcast.keys(), _broadcast)) + else: + broadcast = maybe_broadcast # possibly `{}` + + out = {**never_broadcast, **broadcast} + + return [out[ix] for ix in range(len(args))] + + def guess_intercept( y, sample_weight, @@ -1577,59 +1633,3 @@ def guess_intercept( else: return link.link(y.dot(sample_weight)) - - -def get_one_over_variance( - distribution: ExponentialDispersionModel, - link: Link, - mu, - eta, - dispersion: float, - sample_weight, -): - """Get one over the variance. - - For Tweedie: ``sigma_inv = sample_weight / (mu ** p)`` during optimization, - because ``phi = 1``. - - For Binomial with Logit link: Simplifies to - ``variance = phi / ( sample_weight * (exp(eta) + 2 + exp(-eta)))``. - More numerically accurate. - """ - if isinstance(distribution, BinomialDistribution) and isinstance(link, LogitLink): - max_float_for_exp = np.log(np.finfo(eta.dtype).max / 10) - if np.any(np.abs(eta) > max_float_for_exp): - eta = np.clip(eta, -max_float_for_exp, max_float_for_exp) # type: ignore - return sample_weight * (np.exp(eta) + 2 + np.exp(-eta)) / dispersion - return 1.0 / distribution.variance( - mu, dispersion=dispersion, sample_weight=sample_weight - ) - - -def _as_float_arrays(*args): - """Convert to a float array, passing ``None`` through, and broadcast.""" - never_broadcast = {} # type: ignore - maybe_broadcast = {} - always_broadcast = {} - - for ix, arg in enumerate(args): - if isinstance(arg, (int, float)): - maybe_broadcast[ix] = np.array([arg], dtype="float") - elif arg is None: - never_broadcast[ix] = None - else: - always_broadcast[ix] = np.asanyarray(arg, dtype="float") - - if always_broadcast and maybe_broadcast: - to_broadcast = {**always_broadcast, **maybe_broadcast} - _broadcast = np.broadcast_arrays(*to_broadcast.values()) - broadcast = dict(zip(to_broadcast.keys(), _broadcast)) - elif always_broadcast: - _broadcast = np.broadcast_arrays(*always_broadcast.values()) - broadcast = dict(zip(always_broadcast.keys(), _broadcast)) - else: - broadcast = maybe_broadcast # possibly `{}` - - out = {**never_broadcast, **broadcast} - - return [out[ix] for ix in range(len(args))] diff --git a/src/glum/_formula.py b/src/glum/_formula.py new file mode 100644 index 000000000..f630028d5 --- /dev/null +++ b/src/glum/_formula.py @@ -0,0 +1,55 @@ +from collections.abc import Mapping +from typing import Any, Optional + +import formulaic + + +def capture_context(context) -> Optional[Mapping[str, Any]]: + if isinstance(context, int): + context = context + 2 + return formulaic.utils.context.capture_context(context) + + +def parse_formula( + formula: formulaic.FormulaSpec, include_intercept: bool = True +) -> tuple[Optional[formulaic.Formula], formulaic.Formula]: + """ + Parse and transform the formula for use in a GeneralizedLinearRegressor. + + The left-hand side and right-hand side of the formula are separated. If an + intercept is present, it will be removed from the right-hand side, and a + boolean flag to indicate whether or not an intercept should be added to + the model will be returned. + + Parameters + ---------- + formula : formulaic.FormulaSpec + The formula to parse. + include_intercept: bool, default True + Whether to include an intercept column. + + Returns + ------- + tuple[formulaic.Formula, formulaic.Formula] + The left-hand side and right-hand sides of the formula. + """ + if isinstance(formula, str): + terms = formulaic.parser.DefaultFormulaParser( + include_intercept=include_intercept + ).get_terms(formula) + elif isinstance(formula, formulaic.Formula): + terms = formula + else: + raise TypeError("formula must be a string or Formula object.") + + if hasattr(terms, "lhs"): + lhs_terms = terms.lhs + rhs_terms = terms.rhs + if len(lhs_terms) != 1: + msg = "formula must have exactly one term on the left-hand side." + raise ValueError(msg) + else: + lhs_terms = None + rhs_terms = terms + + return lhs_terms, rhs_terms diff --git a/src/glum/_functions.pyx b/src/glum/_functions.pyx index f19911fcb..ea70effb1 100644 --- a/src/glum/_functions.pyx +++ b/src/glum/_functions.pyx @@ -593,4 +593,4 @@ def negative_binomial_deviance( if y[i] > 0: D += weights[i] * y[i] * log(y[i] / mu[i]) - return 2 * D \ No newline at end of file + return 2 * D diff --git a/src/glum/_glm.py b/src/glum/_glm.py index 3397d15f3..98b5700c5 100644 --- a/src/glum/_glm.py +++ b/src/glum/_glm.py @@ -1,47 +1,19 @@ -""" -Generalized Linear Models with Exponential Dispersion Family - -Modified from code submitted as a PR to sklearn: -https://github.com/scikit-learn/scikit-learn/pull/9405 - -Original attribution from: -https://github.com/scikit-learn/scikit-learn/pull/9405/files -#diff-38e412190dc50455611b75cfcf2d002713dcf6d537a78b9a22cc6b1c164390d1 -''' -Author: Christian Lorentzen -some parts and tricks stolen from other sklearn files. -''' -""" - -# License: BSD 3 clause -import copy import re import sys +import time +import typing import warnings -from collections.abc import Iterable, Mapping, Sequence -from time import perf_counter -from typing import Any, NamedTuple, Optional, Union, cast +from collections.abc import Mapping, Sequence +from typing import Any, Optional, Union +import formulaic import numpy as np +import packaging.version import pandas as pd import scipy.sparse as sps -import scipy.sparse.linalg as splinalg import sklearn as skl import tabmat as tm -from formulaic import Formula, FormulaSpec -from formulaic.parser import DefaultFormulaParser -from formulaic.utils.constraints import LinearConstraintParser -from formulaic.utils.context import capture_context -from packaging import version from scipy import linalg, sparse, stats -from sklearn.base import BaseEstimator, RegressorMixin -from sklearn.utils import check_array -from sklearn.utils.validation import ( - assert_all_finite, - check_consistent_length, - check_is_fitted, - column_or_1d, -) from ._distribution import ( BinomialDistribution, @@ -55,6 +27,7 @@ TweedieDistribution, guess_intercept, ) +from ._formula import capture_context, parse_formula from ._link import CloglogLink, IdentityLink, Link, LogitLink, LogLink, TweedieLink from ._solvers import ( IRLSData, @@ -64,889 +37,268 @@ _least_squares_solver, _trust_constr_solver, ) -from ._util import ( - _add_missing_categories, - _align_df_categories, - _expand_categorical_penalties, - _is_contiguous, - _safe_toarray, +from ._typing import ArrayLike, ShapedArrayLike, VectorLike, WaldTestResult +from ._utils import ( + add_missing_categories, + align_df_categories, + expand_categorical_penalties, + is_contiguous, + safe_toarray, + standardize_warm_start, +) +from ._validation import ( + check_array_tabmat_compliant, + check_offset, + check_weights, + check_X_y_tabmat_compliant, ) -if version.parse(skl.__version__).release < (1, 6): +if packaging.version.parse(skl.__version__).release < (1, 6): keyword_finiteness = "force_all_finite" - validate_data = BaseEstimator._validate_data + validate_data = skl.base.BaseEstimator._validate_data else: keyword_finiteness = "ensure_all_finite" from sklearn.utils.validation import validate_data # type: ignore _float_itemsize_to_dtype = {8: np.float64, 4: np.float32, 2: np.float16} -VectorLike = Union[np.ndarray, pd.api.extensions.ExtensionArray, pd.Index, pd.Series] - -ArrayLike = Union[ - list, - tm.MatrixBase, - tm.StandardizedMatrix, - pd.DataFrame, - sparse.spmatrix, - VectorLike, -] - -ShapedArrayLike = Union[ - tm.MatrixBase, - tm.StandardizedMatrix, - pd.DataFrame, - sparse.spmatrix, - VectorLike, -] - - -class WaldTestResult(NamedTuple): - test_statistic: float - p_value: float - df: int - - -def check_array_tabmat_compliant(mat: ArrayLike, drop_first: bool = False, **kwargs): - to_copy = kwargs.get("copy", False) - - if isinstance(mat, pd.DataFrame): - raise RuntimeError("DataFrames should have been converted by this point.") - - if isinstance(mat, tm.SplitMatrix): - kwargs.update({"ensure_min_features": 0}) - new_matrices = [ - check_array_tabmat_compliant(m, drop_first=drop_first, **kwargs) - for m in mat.matrices - ] - new_indices = [elt.copy() for elt in mat.indices] if to_copy else mat.indices - return tm.SplitMatrix(new_matrices, new_indices) - - if isinstance(mat, tm.CategoricalMatrix): - if to_copy: - return copy.copy(mat) - return mat - - if isinstance(mat, tm.StandardizedMatrix): - return tm.StandardizedMatrix( - check_array_tabmat_compliant(mat.mat, drop_first=drop_first, **kwargs), - check_array(mat.shift, **kwargs), - ) - - original_type = type(mat) - if isinstance(mat, (tm.DenseMatrix, tm.SparseMatrix)): - res = check_array(mat.unpack(), **kwargs) - else: - res = check_array(mat, **kwargs) - - if res is not mat and original_type in (tm.DenseMatrix, tm.SparseMatrix): - res = original_type( - res, - column_names=mat.column_names, # type: ignore - term_names=mat.term_names, # type: ignore - ) - - return res - -def check_X_y_tabmat_compliant( - X: ArrayLike, y: Union[VectorLike, sparse.spmatrix], **kwargs -) -> tuple[Union[tm.MatrixBase, sparse.spmatrix, np.ndarray], np.ndarray]: +class GeneralizedLinearRegressorBase(skl.base.RegressorMixin, skl.base.BaseEstimator): """ - See the documentation for :func:`sklearn.utils.check_X_y`. This function - behaves identically for inputs that are not from the Matrix package and - fixes some parameters, such as ``'force_all_finite'``, to match the needs of - GLMs. - - Returns - ------- - X_converted : array-like - The converted and validated X. - y_converted : numpy.ndarray - The converted and validated y. + Base class for :class:`GeneralizedLinearRegressor` and + :class:`GeneralizedLinearRegressorCV`. """ - if y is None: - raise ValueError("y cannot be None") - - y = column_or_1d(y, warn=True) - assert_all_finite(y) - if y.dtype.kind == "O": - y = y.astype(np.float64) - - check_consistent_length(X, y) - X = check_array_tabmat_compliant(X, **kwargs) - - return X, y - - -def _check_weights( - sample_weight: Optional[Union[float, VectorLike]], - n_samples: int, - dtype, - force_all_finite: bool = True, -) -> np.ndarray: - """Check that sample weights are non-negative and have the right shape.""" - if sample_weight is None: - return np.ones(n_samples, dtype=dtype) - if np.isscalar(sample_weight): - if sample_weight <= 0: # type: ignore - raise ValueError("Sample weights must be non-negative.") - return np.full(n_samples, sample_weight, dtype=dtype) - - sample_weight = check_array( - sample_weight, - accept_sparse=False, - ensure_2d=False, - dtype=[np.float64, np.float32], - **{keyword_finiteness: force_all_finite}, - ) - - if sample_weight.ndim > 1: # type: ignore - raise ValueError("Sample weights must be 1D array or scalar.") - if sample_weight.shape[0] != n_samples: # type: ignore - raise ValueError("Sample weights must have the same length as y.") - if np.any(sample_weight < 0): # type: ignore - raise ValueError("Sample weights must be non-negative.") - if np.sum(sample_weight) == 0: # type: ignore - raise ValueError("Sample weights must have at least one positive element.") - return sample_weight # type: ignore - - -def _check_offset( - offset: Optional[Union[VectorLike, float]], n_rows: int, dtype -) -> Optional[np.ndarray]: - """ - Unlike weights, if the offset is ``None``, it can stay ``None``, so we only - need to validate it when it is not. - """ - if offset is None: - return None - if np.isscalar(offset): - return np.full(n_rows, offset) - - offset = check_array( - offset, - accept_sparse=False, - ensure_2d=False, - dtype=dtype, - **{keyword_finiteness: True}, - ) + def __init__( + self, + *, + l1_ratio: float = 0, + P1: Optional[Union[str, np.ndarray]] = "identity", + P2: Optional[Union[str, np.ndarray, sparse.spmatrix]] = "identity", + fit_intercept=True, + family: Union[str, ExponentialDispersionModel] = "normal", + link: Union[str, Link] = "auto", + solver: str = "auto", + max_iter=100, + max_inner_iter=100000, + gradient_tol: Optional[float] = None, + step_size_tol: Optional[float] = None, + hessian_approx: float = 0.0, + warm_start=False, + alpha_search: bool = False, + n_alphas: int = 100, + min_alpha_ratio: Optional[float] = None, + min_alpha: Optional[float] = None, + start_params: Optional[np.ndarray] = None, + selection="cyclic", + random_state=None, + copy_X: Optional[bool] = None, + check_input=True, + verbose=0, + scale_predictors: bool = False, + lower_bounds: Optional[np.ndarray] = None, + upper_bounds: Optional[np.ndarray] = None, + A_ineq: Optional[np.ndarray] = None, + b_ineq: Optional[np.ndarray] = None, + force_all_finite: bool = True, + drop_first: bool = False, + robust: bool = True, + expected_information: bool = False, + formula: Optional[formulaic.FormulaSpec] = None, + interaction_separator: str = ":", + categorical_format: str = "{name}[{category}]", + cat_missing_method: str = "fail", + cat_missing_name: str = "(MISSING)", + ): + self.l1_ratio = l1_ratio + self.P1 = P1 + self.P2 = P2 + self.fit_intercept = fit_intercept + self.family = family + self.link = link + self.solver = solver + self.max_iter = max_iter + self.max_inner_iter = max_inner_iter + self.gradient_tol = gradient_tol + self.step_size_tol = step_size_tol + self.hessian_approx = hessian_approx + self.warm_start = warm_start + self.alpha_search = alpha_search + self.n_alphas = n_alphas + self.min_alpha_ratio = min_alpha_ratio + self.min_alpha = min_alpha + self.start_params = start_params + self.selection = selection + self.random_state = random_state + self.copy_X = copy_X + self.check_input = check_input + self.verbose = verbose + self.scale_predictors = scale_predictors + self.lower_bounds = lower_bounds + self.upper_bounds = upper_bounds + self.A_ineq = A_ineq + self.b_ineq = b_ineq + self.force_all_finite = force_all_finite + self.drop_first = drop_first + self.robust = robust + self.expected_information = expected_information + self.formula = formula + self.interaction_separator = interaction_separator + self.categorical_format = categorical_format + self.cat_missing_method = cat_missing_method + self.cat_missing_name = cat_missing_name - offset = cast(np.ndarray, offset) + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + tags.input_tags.sparse = True + return tags - if offset.ndim > 1: # type: ignore - raise ValueError("Offsets must be 1D array or scalar.") - if offset.shape[0] != n_rows: # type: ignore - raise ValueError("Offsets must have the same length as y.") + @property + def family_instance(self) -> ExponentialDispersionModel: + """Return an :class:`~glum._distribution.ExponentialDispersionModel`.""" + if hasattr(self, "_family_instance"): + return self._family_instance + else: + return get_family(self.family) - return offset + @property + def link_instance(self) -> Link: + """Return a :class:`~glum._link.Link`.""" + if hasattr(self, "_link_instance"): + return self._link_instance + else: + return get_link(self.link, self.family_instance) + def _get_start_coef( + self, + X: Union[tm.MatrixBase, tm.StandardizedMatrix], + y: np.ndarray, + sample_weight: np.ndarray, + offset: Optional[np.ndarray], + col_means: np.ndarray, + col_stds: Optional[np.ndarray], + dtype, + ) -> np.ndarray: + if self.warm_start and hasattr(self, "coef_"): + coef = self.coef_ # type: ignore + intercept = self.intercept_ # type: ignore + if self.fit_intercept: + coef = np.concatenate((np.array([intercept]), coef)) + if self._center_predictors: + standardize_warm_start(coef, col_means, col_stds) # type: ignore -def _parse_formula( - formula: FormulaSpec, include_intercept: bool = True -) -> tuple[Optional[Formula], Formula]: - """ - Parse and transform the formula for use in a GeneralizedLinearRegressor. - - The left-hand side and right-hand side of the formula are separated. If an - intercept is present, it will be removed from the right-hand side, and a - boolean flag to indicate whether or not an intercept should be added to - the model will be returned. - - Parameters - ---------- - formula : formulaic.FormulaSpec - The formula to parse. - include_intercept: bool, default True - Whether to include an intercept column. - - Returns - ------- - tuple[formulaic.Formula, formulaic.Formula] - The left-hand side and right-hand sides of the formula. - """ - if isinstance(formula, str): - parser = DefaultFormulaParser(include_intercept=include_intercept) - terms = parser.get_terms(formula) - elif isinstance(formula, Formula): - terms = formula - else: - raise TypeError("formula must be a string or Formula object.") + elif self.start_params is None: + if self.fit_intercept: + coef = np.zeros( + X.shape[1] + 1, dtype=_float_itemsize_to_dtype[X.dtype.itemsize] + ) + coef[0] = guess_intercept( + y, sample_weight, self._link_instance, self._family_instance, offset + ) + else: + coef = np.zeros( + X.shape[1], dtype=_float_itemsize_to_dtype[X.dtype.itemsize] + ) - if hasattr(terms, "lhs"): - lhs_terms = terms.lhs - if len(lhs_terms) != 1: - raise ValueError( - "formula must have exactly one term on the left-hand side." + else: # assign given array as start values + coef = skl.utils.check_array( + self.start_params, + accept_sparse=False, + ensure_2d=False, + dtype=dtype, + copy=True, + **{keyword_finiteness: True}, ) - rhs_terms = terms.rhs - else: - lhs_terms = None - rhs_terms = terms - - return lhs_terms, rhs_terms - - -def check_bounds( - bounds: Optional[Union[float, VectorLike]], n_features: int, dtype -) -> Optional[np.ndarray]: - """Check that the bounds have the right shape.""" - if bounds is None: - return None - if np.isscalar(bounds): - return np.full(n_features, bounds, dtype=dtype) - - bounds = check_array( - bounds, - accept_sparse=False, - ensure_2d=False, - dtype=dtype, - **{keyword_finiteness: False}, - ) - bounds = cast(np.ndarray, bounds) + if coef.shape != (len(col_means) + self.fit_intercept,): + raise ValueError( + "Start values for parameters must have the right length " + f"and dimension; got {coef.shape}, needed " + f"({len(col_means) + self.fit_intercept},)." + ) - if bounds.ndim > 1: # type: ignore - raise ValueError("Bounds must be 1D array or scalar.") - if bounds.shape[0] != n_features: # type: ignore - raise ValueError("Bounds must be the same length as X.shape[1].") + if self._center_predictors: + standardize_warm_start(coef, col_means, col_stds) # type: ignore - return bounds + # If starting values are outside the specified bounds (if set), + # bring the starting value exactly at the bound. + idx = int(self.fit_intercept) + if self.lower_bounds is not None: + if np.any(coef[idx:] < self.lower_bounds): + warnings.warn( + "lower_bounds above starting value. Setting the starting values " + "to max(start_params, lower_bounds)." + ) + coef[idx:] = np.maximum(coef[idx:], self.lower_bounds) + if self.upper_bounds is not None: + if np.any(coef[idx:] > self.upper_bounds): + warnings.warn( + "upper_bounds below starting value. Setting the starting values " + "to min(start_params, upper_bounds)." + ) + coef[idx:] = np.minimum(coef[idx:], self.upper_bounds) + return coef -def check_inequality_constraints( - A_ineq: Optional[np.ndarray], - b_ineq: Optional[np.ndarray], - n_features: int, - dtype, -) -> tuple[Union[None, np.ndarray], Union[None, np.ndarray]]: - """Check that the inequality constraints are well-defined.""" - if A_ineq is None or b_ineq is None: - return None, None - else: - A_ineq = check_array( - A_ineq, - accept_sparse=False, - ensure_2d=True, - dtype=dtype, - copy=True, - **{keyword_finiteness: False}, - ) - b_ineq = check_array( - b_ineq, - accept_sparse=False, - ensure_2d=False, - dtype=dtype, - copy=True, - **{keyword_finiteness: False}, - ) - if A_ineq.shape[1] != n_features: # type: ignore - raise ValueError("A_ineq must have same number of columns as X.") - if A_ineq.shape[0] != b_ineq.shape[0]: # type: ignore - raise ValueError("A_ineq and b_ineq must have same number of rows.") - if b_ineq.ndim > 1: # type: ignore - raise ValueError("b_ineq must be 1D array.") - return A_ineq, b_ineq - - -def _standardize( - X: tm.MatrixBase, - sample_weight: np.ndarray, - center_predictors: bool, - estimate_as_if_scaled_model: bool, - lower_bounds: Optional[np.ndarray], - upper_bounds: Optional[np.ndarray], - A_ineq: Optional[np.ndarray], - P1: Union[np.ndarray, sparse.spmatrix], - P2: Union[np.ndarray, sparse.spmatrix], -) -> tuple[ - tm.StandardizedMatrix, - np.ndarray, - Optional[np.ndarray], - Optional[np.ndarray], - Optional[np.ndarray], - Optional[np.ndarray], - Any, - Any, -]: - """ - Standardize the data matrix ``X`` and adjust the bounds and penalties to - match the standardized data matrix, so that standardizing does not affect - estimates. - - This is only done for computational reasons and does not affect final - estimates or alter the input data. Columns are always scaled to have unit - standard deviation. - - Bounds, inequality constraints and regularization coefficients are modified - appropriately so that the estimates remain unchanged compared to an - unstandardized problem. - - Parameters - ---------- - X : MatrixBase - sample_weight : numpy.ndarray - center_predictors : bool - If ``True``, adjust the data matrix so that columns have mean zero. - estimate_as_if_scaled_model : bool - If ``True``, estimates returned equal those from a model where - predictors have been standardized to have unit standard deviation, with - penalty unchanged. Note that, internally, for purely computational - reasons, we always scale predictors; whether estimates match a scaled - model depends on whether we modify the penalty. If ``False``, penalties - are rescaled to match the original scale, canceling out the effect of - rescaling X. - lower_bounds - upper_bounds - A_ineq - P1 - P2 - """ - X, col_means, col_stds = X.standardize(sample_weight, center_predictors, True) - - if col_stds is not None: - inv_col_stds = _one_over_var_inf_to_val(col_stds, 1.0) - # We copy the bounds when multiplying here so the we avoid - # side effects. - if lower_bounds is not None: - lower_bounds = lower_bounds / inv_col_stds - if upper_bounds is not None: - upper_bounds = upper_bounds / inv_col_stds - if A_ineq is not None: - A_ineq = A_ineq * inv_col_stds - - if not estimate_as_if_scaled_model and col_stds is not None: - P1 *= inv_col_stds - if sparse.issparse(P2): - inv_col_stds_mat = sparse.diags(inv_col_stds) - P2 = inv_col_stds_mat @ P2 @ inv_col_stds_mat - elif P2.ndim == 1: - P2 *= inv_col_stds**2 - else: - P2 = (inv_col_stds[:, None] * P2) * inv_col_stds[None, :] + def _convert_from_pandas( + self, + df: pd.DataFrame, + context: Optional[Mapping[str, Any]] = None, + ) -> tm.MatrixBase: + """Convert a pandas data frame to a tabmat matrix.""" + if hasattr(self, "X_model_spec_"): + return self.X_model_spec_.get_model_matrix(df, context=context) - return X, col_means, col_stds, lower_bounds, upper_bounds, A_ineq, P1, P2 + cat_missing_method_after_alignment = getattr(self, "cat_missing_method", "fail") + if hasattr(self, "feature_dtypes_"): + df = align_df_categories( + df, + self.feature_dtypes_, + getattr(self, "has_missing_category_", {}), + cat_missing_method_after_alignment, + ) + if cat_missing_method_after_alignment == "convert": + df = add_missing_categories( + df=df, + dtypes=self.feature_dtypes_, + feature_names=self.feature_names_, + cat_missing_name=self.cat_missing_name, + categorical_format=self.categorical_format, + ) + # there should be no missing categories after this + cat_missing_method_after_alignment = "fail" -def _unstandardize( - col_means: np.ndarray, - col_stds: Optional[np.ndarray], - intercept: Union[float, np.ndarray], - coef: np.ndarray, -) -> tuple[Union[float, np.ndarray], np.ndarray]: - if col_stds is None: - intercept -= np.squeeze(np.squeeze(col_means).dot(np.atleast_1d(coef).T)) - else: - penalty_mult = _one_over_var_inf_to_val(col_stds, 1.0) - intercept -= np.squeeze( - np.squeeze(col_means * penalty_mult).dot(np.atleast_1d(coef).T) + X = tm.from_pandas( + df, + drop_first=self.drop_first, + categorical_format=getattr( # convention prior to v3 + self, "categorical_format", "{name}__{category}" + ), + cat_missing_method=cat_missing_method_after_alignment, ) - coef *= penalty_mult - return intercept, coef + return X -def _one_over_var_inf_to_val(arr: np.ndarray, val: float) -> np.ndarray: - """ - Return 1/arr unless the values are zeros. - - If values are zeros, return val. - """ - zeros = np.where(np.abs(arr) < 10 * np.sqrt(np.finfo(arr.dtype).eps)) - with np.errstate(divide="ignore"): - one_over = 1 / arr - one_over[zeros] = val - return one_over - - -def _standardize_warm_start( - coef: np.ndarray, col_means: np.ndarray, col_stds: Optional[np.ndarray] -) -> None: - if col_stds is None: - coef[0] += np.squeeze(col_means).dot(coef[1:]) - else: - coef[1:] *= col_stds - coef[0] += np.squeeze(col_means * _one_over_var_inf_to_val(col_stds, 1)).dot( - coef[1:] - ) + def _set_up_for_fit(self, y: np.ndarray) -> None: + ####################################################################### + # 1. input validation # + ####################################################################### + # self.family and self.link are user-provided inputs and may be strings or + # ExponentialDispersonModel/Link objects + # self.family_instance_ and self.link_instance_ are cleaned by 'fit' to be + # ExponentialDispersionModel and Link arguments + self._family_instance: ExponentialDispersionModel = get_family(self.family) + # Guarantee that self._link_instance is set to an instance of class Link + self._link_instance: Link = get_link(self.link, self._family_instance) + # when fit_intercept is False, we can't center because that would + # substantially change estimates + self._center_predictors: bool = self.fit_intercept -def get_family( - family: Union[str, ExponentialDispersionModel], -) -> ExponentialDispersionModel: - if isinstance(family, ExponentialDispersionModel): - return family - - name_to_dist = { - "binomial": BinomialDistribution(), - "gamma": GammaDistribution(), - "gaussian": NormalDistribution(), - "inverse.gaussian": InverseGaussianDistribution(), - "normal": NormalDistribution(), - "poisson": PoissonDistribution(), - "tweedie": TweedieDistribution(1.5), - "negative.binomial": NegativeBinomialDistribution(1.0), - } - - if family in name_to_dist: - return name_to_dist[family] - - custom_tweedie = re.search(r"tweedie\s?\((.+)\)", family) - - if custom_tweedie: - return TweedieDistribution(float(custom_tweedie.group(1))) - - custom_negative_binomial = re.search(r"negative.binomial\s?\((.+)\)", family) - - if custom_negative_binomial: - return NegativeBinomialDistribution(float(custom_negative_binomial.group(1))) - - raise ValueError( - "The family must be an instance of class ExponentialDispersionModel or an " - f"element of {sorted(name_to_dist.keys())}; got (family={family})." - ) - - -def get_link(link: Union[str, Link], family: ExponentialDispersionModel) -> Link: - """ - For the Tweedie distribution, this code follows actuarial best practices - regarding link functions. Note that these links are sometimes not canonical: - - identity for normal (``p = 0``); - - no convention for ``p < 0``, so let's leave it as identity; - - log otherwise. - """ - if isinstance(link, Link): - return link - - if (link is None) or (link == "auto"): - if tweedie_representation := family.to_tweedie(safe=False): - if tweedie_representation.power <= 0: - return IdentityLink() - return LogLink() - if isinstance(family, GeneralizedHyperbolicSecant): - return IdentityLink() - if isinstance(family, BinomialDistribution): - return LogitLink() - if isinstance(family, NegativeBinomialDistribution): - return LogLink() - raise ValueError( - "No default link known for the specified distribution family. " - "Please set link manually, i.e. not to 'auto'. " - f"Got (link='auto', family={family.__class__.__name__})." - ) - - mapping = { - "cloglog": CloglogLink(), - "identity": IdentityLink(), - "log": LogLink(), - "logit": LogitLink(), - "tweedie": TweedieLink(1.5), - } - - if link in mapping: - return mapping[link] - if custom_tweedie := re.search(r"tweedie\s?\((.+)\)", link): - return TweedieLink(float(custom_tweedie.group(1))) - - raise ValueError( - "The link must be an instance of class Link or an element of " - "['auto', 'identity', 'log', 'logit', 'cloglog', 'tweedie']; " - f"got (link={link})." - ) - - -def setup_p1( - P1: Optional[Union[str, np.ndarray]], - X: Union[tm.MatrixBase, tm.StandardizedMatrix], - dtype, - alpha: float, - l1_ratio: float, -) -> np.ndarray: - if not isinstance(X, (tm.MatrixBase, tm.StandardizedMatrix)): - raise TypeError - - n_features = X.shape[1] - - if isinstance(P1, str): - if P1 != "identity": - raise ValueError(f"P1 must be either 'identity' or an array; got {P1}.") - P1 = np.ones(n_features, dtype=dtype) - elif P1 is None: - P1 = np.ones(n_features, dtype=dtype) - else: - P1 = np.atleast_1d(P1) - try: - P1 = P1.astype(dtype, casting="safe", copy=False) # type: ignore - except TypeError as e: - raise TypeError( - "The given P1 cannot be converted to a numeric array; " - f"got (P1.dtype={P1.dtype})." # type: ignore - ) from e - if (P1.ndim != 1) or (P1.shape[0] != n_features): # type: ignore - raise ValueError( - "P1 must be either 'identity' or a 1d array with the length of " - "X.shape[1] (either before or after categorical expansion); " - f"got (P1.shape[0]={P1.shape[0]})." # type: ignore - ) - - # P1 and P2 are now for sure copies - P1 = alpha * l1_ratio * P1 # type: ignore - return cast(np.ndarray, P1).astype(dtype) - - -def setup_p2( - P2: Optional[Union[str, np.ndarray, sparse.spmatrix]], - X: Union[tm.MatrixBase, tm.StandardizedMatrix], - stype, - dtype, - alpha: float, - l1_ratio: float, -) -> Union[np.ndarray, sparse.spmatrix]: - if not isinstance(X, (tm.MatrixBase, tm.StandardizedMatrix)): - raise TypeError - - n_features = X.shape[1] - - def _setup_sparse_p2(P2): - return (sparse.dia_matrix((P2, 0), shape=(n_features, n_features))).tocsc() - - if isinstance(P2, str): - if P2 != "identity": - raise ValueError(f"P2 must be either 'identity' or an array. Got {P2}.") - if sparse.issparse(X): # if X is sparse, make P2 sparse, too - P2 = _setup_sparse_p2(np.ones(n_features, dtype=dtype)) - else: - P2 = np.ones(n_features, dtype=dtype) - elif P2 is None: - if sparse.issparse(X): # if X is sparse, make P2 sparse, too - P2 = _setup_sparse_p2(np.ones(n_features, dtype=dtype)) - else: - P2 = np.ones(n_features, dtype=dtype) - else: - P2 = check_array( - P2, copy=True, accept_sparse=stype, dtype=dtype, ensure_2d=False - ) - P2 = cast(np.ndarray, P2) - if P2.ndim == 1: - P2 = np.asarray(P2) - if P2.shape[0] != n_features: - raise ValueError( - "P2 should be a 1d array of shape X.shape[1] (either before or " - "after categorical expansion); " - f"got (P2.shape={P2.shape})." - ) - if sparse.issparse(X): - P2 = _setup_sparse_p2(P2) - elif P2.ndim == 2 and P2.shape[0] == P2.shape[1] and P2.shape[0] == n_features: - if sparse.issparse(X): - P2 = sparse.csc_matrix(P2) - else: - raise ValueError( - "P2 must be either None or an array of shape (n_features, n_features) " - f"with n_features=X.shape[1]; got (P2.shape={P2.shape}); " - f"needed ({n_features}, {n_features})." - ) - - # P1 and P2 are now for sure copies - P2 = alpha * (1 - l1_ratio) * P2 - # one only ever needs the symmetrized L2 penalty matrix 1/2 (P2 + P2') - # reason: w' P2 w = (w' P2 w)', i.e. it is symmetric - if P2.ndim == 2: - if sparse.issparse(P2): - if sparse.isspmatrix_csc(P2): - P2 = 0.5 * (P2 + P2.transpose()).tocsc() - else: - P2 = 0.5 * (P2 + P2.transpose()).tocsr() - else: - P2 = 0.5 * (P2 + P2.T) - return P2 - - -def is_pos_semidef(p: Union[sparse.spmatrix, np.ndarray]) -> Union[bool, np.bool_]: - """ - Checks for positive semidefiniteness of ``p`` if ``p`` is a matrix, or - ``diag(p)`` if a vector. - - ``np.linalg.cholesky(P2)`` 'only' asserts positive definiteness; due to - numerical precision, we allow eigenvalues to be a tiny bit negative. - """ - # 1d case - if p.ndim == 1 or p.shape[0] == 1: - any_negative = (p < 0).max() if sparse.isspmatrix(p) else (p < 0).any() - return not any_negative - - # 2d case - # About -6e-7 for 32-bit, -1e-15 for 64-bit - epsneg = -10 * np.finfo(np.result_type(float, p.dtype)).epsneg - - if sparse.issparse(p): - # Computing eigenvalues for sparse matrices is inefficient. If the matrix is - # not huge, convert to dense. Otherwise, calculate 10% of its eigenvalues. - p = cast(sparse.spmatrix, p) - if p.shape[0] < 2000: - eigenvalues = linalg.eigvalsh(p.toarray()) - else: - n_evals_to_compuate = p.shape[0] // 10 + 1 - sigma = -1000 * epsneg # start searching near this value - which = "SA" # find smallest algebraic eigenvalues first - eigenvalues = splinalg.eigsh( - p, - k=n_evals_to_compuate, - sigma=sigma, - which=which, - return_eigenvectors=False, - ) - else: # dense - eigenvalues = linalg.eigvalsh(p) - - return np.all(eigenvalues >= epsneg) - - -def _group_sum(groups: np.ndarray, data: tm.MatrixBase): - """Sum over groups.""" - ngroups = len(np.unique(groups)) - out = np.empty((ngroups, data.shape[1])) - eye_n = sps.eye(ngroups, format="csc")[:, groups] - for i in range(data.shape[1]): - out[:, i] = _safe_toarray(eye_n @ data.getcol(i).unpack()).ravel() - return out - - -# TODO: abc -class GeneralizedLinearRegressorBase(RegressorMixin, BaseEstimator): - """ - Base class for :class:`GeneralizedLinearRegressor` and - :class:`GeneralizedLinearRegressorCV`. - """ - - def __init__( - self, - *, - l1_ratio: float = 0, - P1: Optional[Union[str, np.ndarray]] = "identity", - P2: Optional[Union[str, np.ndarray, sparse.spmatrix]] = "identity", - fit_intercept=True, - family: Union[str, ExponentialDispersionModel] = "normal", - link: Union[str, Link] = "auto", - solver: str = "auto", - max_iter=100, - max_inner_iter=100000, - gradient_tol: Optional[float] = None, - step_size_tol: Optional[float] = None, - hessian_approx: float = 0.0, - warm_start=False, - alpha_search: bool = False, - n_alphas: int = 100, - min_alpha_ratio: Optional[float] = None, - min_alpha: Optional[float] = None, - start_params: Optional[np.ndarray] = None, - selection="cyclic", - random_state=None, - copy_X: Optional[bool] = None, - check_input=True, - verbose=0, - scale_predictors: bool = False, - lower_bounds: Optional[np.ndarray] = None, - upper_bounds: Optional[np.ndarray] = None, - A_ineq: Optional[np.ndarray] = None, - b_ineq: Optional[np.ndarray] = None, - force_all_finite: bool = True, - drop_first: bool = False, - robust: bool = True, - expected_information: bool = False, - formula: Optional[FormulaSpec] = None, - interaction_separator: str = ":", - categorical_format: str = "{name}[{category}]", - cat_missing_method: str = "fail", - cat_missing_name: str = "(MISSING)", - ): - self.l1_ratio = l1_ratio - self.P1 = P1 - self.P2 = P2 - self.fit_intercept = fit_intercept - self.family = family - self.link = link - self.solver = solver - self.max_iter = max_iter - self.max_inner_iter = max_inner_iter - self.gradient_tol = gradient_tol - self.step_size_tol = step_size_tol - self.hessian_approx = hessian_approx - self.warm_start = warm_start - self.alpha_search = alpha_search - self.n_alphas = n_alphas - self.min_alpha_ratio = min_alpha_ratio - self.min_alpha = min_alpha - self.start_params = start_params - self.selection = selection - self.random_state = random_state - self.copy_X = copy_X - self.check_input = check_input - self.verbose = verbose - self.scale_predictors = scale_predictors - self.lower_bounds = lower_bounds - self.upper_bounds = upper_bounds - self.A_ineq = A_ineq - self.b_ineq = b_ineq - self.force_all_finite = force_all_finite - self.drop_first = drop_first - self.robust = robust - self.expected_information = expected_information - self.formula = formula - self.interaction_separator = interaction_separator - self.categorical_format = categorical_format - self.cat_missing_method = cat_missing_method - self.cat_missing_name = cat_missing_name - - def __sklearn_tags__(self): - tags = super().__sklearn_tags__() - tags.input_tags.sparse = True - return tags - - @property - def family_instance(self) -> ExponentialDispersionModel: - """Return an :class:`~glum._distribution.ExponentialDispersionModel`.""" - if hasattr(self, "_family_instance"): - return self._family_instance - else: - return get_family(self.family) - - @property - def link_instance(self) -> Link: - """Return a :class:`~glum._link.Link`.""" - if hasattr(self, "_link_instance"): - return self._link_instance - else: - return get_link(self.link, self.family_instance) - - def _get_start_coef( - self, - X: Union[tm.MatrixBase, tm.StandardizedMatrix], - y: np.ndarray, - sample_weight: np.ndarray, - offset: Optional[np.ndarray], - col_means: np.ndarray, - col_stds: Optional[np.ndarray], - dtype, - ) -> np.ndarray: - if self.warm_start and hasattr(self, "coef_"): - coef = self.coef_ # type: ignore - intercept = self.intercept_ # type: ignore - if self.fit_intercept: - coef = np.concatenate((np.array([intercept]), coef)) - if self._center_predictors: - _standardize_warm_start(coef, col_means, col_stds) # type: ignore - - elif self.start_params is None: - if self.fit_intercept: - coef = np.zeros( - X.shape[1] + 1, dtype=_float_itemsize_to_dtype[X.dtype.itemsize] - ) - coef[0] = guess_intercept( - y, sample_weight, self._link_instance, self._family_instance, offset - ) - else: - coef = np.zeros( - X.shape[1], dtype=_float_itemsize_to_dtype[X.dtype.itemsize] - ) - - else: # assign given array as start values - coef = check_array( - self.start_params, - accept_sparse=False, - ensure_2d=False, - dtype=dtype, - copy=True, - **{keyword_finiteness: True}, - ) - - if coef.shape != (len(col_means) + self.fit_intercept,): - raise ValueError( - "Start values for parameters must have the right length " - f"and dimension; got {coef.shape}, needed " - f"({len(col_means) + self.fit_intercept},)." - ) - - if self._center_predictors: - _standardize_warm_start(coef, col_means, col_stds) # type: ignore - - # If starting values are outside the specified bounds (if set), - # bring the starting value exactly at the bound. - idx = int(self.fit_intercept) - if self.lower_bounds is not None: - if np.any(coef[idx:] < self.lower_bounds): - warnings.warn( - "lower_bounds above starting value. Setting the starting values " - "to max(start_params, lower_bounds)." - ) - coef[idx:] = np.maximum(coef[idx:], self.lower_bounds) - if self.upper_bounds is not None: - if np.any(coef[idx:] > self.upper_bounds): - warnings.warn( - "upper_bounds below starting value. Setting the starting values " - "to min(start_params, upper_bounds)." - ) - coef[idx:] = np.minimum(coef[idx:], self.upper_bounds) - - return coef - - def _convert_from_pandas( - self, df: pd.DataFrame, context: Optional[Mapping[str, Any]] = None - ) -> tm.MatrixBase: - """Convert a pandas data frame to a tabmat matrix.""" - if hasattr(self, "X_model_spec_"): - return self.X_model_spec_.get_model_matrix(df, context=context) - - cat_missing_method_after_alignment = getattr(self, "cat_missing_method", "fail") - - if hasattr(self, "feature_dtypes_"): - df = _align_df_categories( - df, - self.feature_dtypes_, - getattr(self, "has_missing_category_", {}), - cat_missing_method_after_alignment, - ) - if cat_missing_method_after_alignment == "convert": - df = _add_missing_categories( - df=df, - dtypes=self.feature_dtypes_, - feature_names=self.feature_names_, - cat_missing_name=self.cat_missing_name, - categorical_format=self.categorical_format, - ) - # there should be no missing categories after this - cat_missing_method_after_alignment = "fail" - - X = tm.from_pandas( - df, - drop_first=self.drop_first, - categorical_format=getattr( # convention prior to v3 - self, "categorical_format", "{name}__{category}" - ), - cat_missing_method=cat_missing_method_after_alignment, - ) - - return X - - def _set_up_for_fit(self, y: np.ndarray) -> None: - ####################################################################### - # 1. input validation # - ####################################################################### - # self.family and self.link are user-provided inputs and may be strings or - # ExponentialDispersonModel/Link objects - # self.family_instance_ and self.link_instance_ are cleaned by 'fit' to be - # ExponentialDispersionModel and Link arguments - self._family_instance: ExponentialDispersionModel = get_family(self.family) - # Guarantee that self._link_instance is set to an instance of class Link - self._link_instance: Link = get_link(self.link, self._family_instance) - - # when fit_intercept is False, we can't center because that would - # substantially change estimates - self._center_predictors: bool = self.fit_intercept - - # require number of observations in the training data for later - # computation of information criteria - self._num_obs: int = y.shape[0] + # require number of observations in the training data for later + # computation of information criteria + self._num_obs: int = y.shape[0] if self.solver == "auto": if (self.A_ineq is not None) and (self.b_ineq is not None): @@ -1192,7 +544,8 @@ def _solve_regularization_path( P1 = P1_no_alpha * alpha P2 = P2_no_alpha * alpha - tic = perf_counter() + tic = time.perf_counter() + coef = self._solve( X=X, y=y, @@ -1206,7 +559,8 @@ def _solve_regularization_path( A_ineq=A_ineq, b_ineq=b_ineq, ) - toc = perf_counter() + + toc = time.perf_counter() if self.verbose > 0: print( @@ -1218,7 +572,10 @@ def _solve_regularization_path( return self.coef_path_ def report_diagnostics( - self, *, full_report: bool = False, custom_columns: Optional[Iterable] = None + self, + *, + full_report: bool = False, + custom_columns: Optional[Sequence] = None, ) -> None: """Print diagnostics to ``stdout``. @@ -1246,7 +603,10 @@ def report_diagnostics( print(diagnostics) def get_formatted_diagnostics( - self, *, full_report: bool = False, custom_columns: Optional[Iterable] = None + self, + *, + full_report: bool = False, + custom_columns: Optional[Sequence] = None, ) -> Union[str, pd.DataFrame]: """Get formatted diagnostics which can be printed with report_diagnostics. @@ -1280,12 +640,8 @@ def get_formatted_diagnostics( elif full_report: keep_cols = df.columns else: - keep_cols = [ - "convergence", - "n_cycles", - "iteration_runtime", - "intercept", - ] + keep_cols = ["convergence", "n_cycles", "iteration_runtime", "intercept"] + return df[keep_cols] def _find_alpha_index(self, alpha): @@ -1347,7 +703,7 @@ def linear_predictor( array, shape (n_samples, n_alphas) The linear predictor. """ - check_is_fitted(self, "coef_") + skl.utils.validation.check_is_fitted(self, "coef_") if (alpha is not None) and (alpha_index is not None): raise ValueError("Please specify only one of {alpha_index, alpha}.") @@ -1357,10 +713,7 @@ def linear_predictor( alpha_index = [self._find_alpha_index(a) for a in alpha] # type: ignore if isinstance(X, pd.DataFrame): - captured_context = capture_context( - context + 1 if isinstance(context, int) else context - ) - X = self._convert_from_pandas(X, context=captured_context) + X = self._convert_from_pandas(X, context=capture_context(context)) X = check_array_tabmat_compliant( X, @@ -1449,20 +802,19 @@ def predict( Predicted values times ``sample_weight``. """ if isinstance(X, pd.DataFrame): - captured_context = capture_context( - context + 1 if isinstance(context, int) else context - ) - X = self._convert_from_pandas(X, context=captured_context) + X = self._convert_from_pandas(X, context=capture_context(context)) eta = self.linear_predictor( - X, offset=offset, alpha_index=alpha_index, alpha=alpha + X, offset=offset, alpha_index=alpha_index, alpha=alpha, context=context ) - mu = get_link(self.link, get_family(self.family)).inverse(eta) + + mu = self.link_instance.inverse(eta) if sample_weight is None: return mu - sample_weight = _check_weights(sample_weight, X.shape[0], X.dtype) + sample_weight = check_weights(sample_weight, X.shape[0], X.dtype) + return mu * sample_weight def coef_table( @@ -1533,9 +885,6 @@ def coef_table( names = self.feature_names_ beta = self.coef_ - captured_context = capture_context( - context + 1 if isinstance(context, int) else context - ) if (X is None) and (getattr(self, "covariance_matrix_", None) is None): return pd.Series(beta, index=names, name="coef") @@ -1549,7 +898,7 @@ def coef_table( robust=robust, clusters=clusters, expected_information=expected_information, - context=captured_context, + context=capture_context(context), ) significance_level = 1 - confidence_level @@ -1675,10 +1024,6 @@ def wald_test( f"Received {num_lhs_specs} specifications." ) - captured_context = capture_context( - context + 1 if isinstance(context, int) else context - ) - kwargs = { "X": X, "y": y, @@ -1689,7 +1034,7 @@ def wald_test( "robust": robust, "clusters": clusters, "expected_information": expected_information, - "context": captured_context, + "context": capture_context(context), } if R is not None: @@ -1795,14 +1140,17 @@ def _wald_test_formula(self, formula: str, **kwargs) -> WaldTestResult: else: names = self.feature_names_ - parser = LinearConstraintParser(names) + parser = formulaic.utils.constraints.LinearConstraintParser(names) R, r = parser.get_matrix(formula) return self._wald_test_matrix(R=R, r=r, **kwargs) def _wald_test_term_names( - self, terms: Union[str, list[str]], values: Optional[Sequence] = None, **kwargs + self, + terms: Union[str, list[str]], + values: Optional[Sequence] = None, + **kwargs, ) -> WaldTestResult: """ Perform a Wald test for the hypothesis that the coefficients of the @@ -1903,25 +1251,21 @@ def std_errors( no context is added. Set ``context=0`` to make the calling scope available. """ - captured_context = capture_context( - context + 1 if isinstance(context, int) else context + covariance_matrix = self.covariance_matrix( + X=X, + y=y, + sample_weight=sample_weight, + offset=offset, + mu=mu, + dispersion=dispersion, + robust=robust, + clusters=clusters, + expected_information=expected_information, + store_covariance_matrix=store_covariance_matrix, + context=capture_context(context), ) - return np.sqrt( - self.covariance_matrix( - X=X, - y=y, - sample_weight=sample_weight, - offset=offset, - mu=mu, - dispersion=dispersion, - robust=robust, - clusters=clusters, - expected_information=expected_information, - store_covariance_matrix=store_covariance_matrix, - context=captured_context, - ).diagonal() - ) + return np.sqrt(covariance_matrix.diagonal()) def covariance_matrix( self, @@ -2035,10 +1379,6 @@ def covariance_matrix( """ self.covariance_matrix_: Union[np.ndarray, None] - captured_context = capture_context( - context + 1 if isinstance(context, int) else context - ) - if robust is None: _robust = getattr(self, "robust", True) else: @@ -2107,7 +1447,7 @@ def covariance_matrix( # This has to go first because X is modified in the next line if isinstance(X, pd.DataFrame): - X = self._convert_from_pandas(X, context=captured_context) + X = self._convert_from_pandas(X, context=capture_context(context)) X, y = check_X_y_tabmat_compliant( X, @@ -2125,13 +1465,13 @@ def covariance_matrix( if sparse.issparse(X) and not isinstance(X, tm.SparseMatrix): X = tm.SparseMatrix(X) - sample_weight = _check_weights( + sample_weight = check_weights( sample_weight, y.shape[0], X.dtype, force_all_finite=self.force_all_finite, ) - offset = _check_offset(offset, y.shape[0], X.dtype) + offset = check_offset(offset, y.shape[0], X.dtype) sum_weights = np.sum(sample_weight) # type: ignore @@ -2149,7 +1489,7 @@ def covariance_matrix( ) if ( - np.linalg.cond(_safe_toarray(X.sandwich(np.ones(X.shape[0])))) + np.linalg.cond(safe_toarray(X.sandwich(np.ones(X.shape[0])))) > 1 / sys.float_info.epsilon**2 ): raise np.linalg.LinAlgError( @@ -2192,7 +1532,7 @@ def covariance_matrix( correction = sum_weights / ( sum_weights - self.n_features_in_ - int(self.fit_intercept) ) - vcov = linalg.solve(oim, linalg.solve(oim, _safe_toarray(inner_part)).T) + vcov = linalg.solve(oim, linalg.solve(oim, safe_toarray(inner_part)).T) vcov *= correction else: fisher = self._family_instance._fisher_information( @@ -2204,7 +1544,7 @@ def covariance_matrix( dispersion, self.fit_intercept, ) - vcov = linalg.inv(_safe_toarray(fisher)) + vcov = linalg.inv(safe_toarray(fisher)) vcov *= sum_weights / ( sum_weights - self.n_features_in_ - int(self.fit_intercept) ) @@ -2265,20 +1605,15 @@ def score( float D^2 of self.predict(X) w.r.t. y. """ - # Note, default score defined in RegressorMixin is R^2 score. - # TODO: make D^2 a score function in module metrics (and thereby get - # input validation and so on) - captured_context = capture_context( - context + 1 if isinstance(context, int) else context - ) + sample_weight = check_weights(sample_weight, y.shape[0], y.dtype) - sample_weight = _check_weights(sample_weight, y.shape[0], y.dtype) - mu = self.predict(X, offset=offset, context=captured_context) - family = get_family(self.family) - dev = family.deviance(y, mu, sample_weight=sample_weight) + mu = self.predict(X, offset=offset, context=context) y_mean = np.average(y, weights=sample_weight) - dev_null = family.deviance(y, y_mean, sample_weight=sample_weight) - return 1.0 - dev / dev_null + + dev = self.family_instance.deviance(y, mu, sample_weight=sample_weight) + dev_null = self.family_instance.deviance(y, y_mean, sample_weight=sample_weight) + + return 1 - dev / dev_null def _validate_hyperparameters(self) -> None: if not isinstance(self.fit_intercept, bool): @@ -2408,7 +1743,7 @@ def _set_up_and_check_fit_args( if isinstance(X, pd.DataFrame): if hasattr(self, "formula") and self.formula is not None: - lhs, rhs = _parse_formula( + lhs, rhs = parse_formula( self.formula, include_intercept=self.fit_intercept ) @@ -2471,10 +1806,10 @@ def _set_up_and_check_fit_args( } if any(X.dtypes == "category"): - P1 = _expand_categorical_penalties( + P1 = expand_categorical_penalties( self.P1, X, drop_first, self.has_missing_category_ ) - P2 = _expand_categorical_penalties( + P2 = expand_categorical_penalties( self.P2, X, drop_first, self.has_missing_category_ ) @@ -2494,7 +1829,7 @@ def _set_up_and_check_fit_args( "requires y to be passed, but the target y is None." ) - if not _is_contiguous(X): + if not is_contiguous(X): if self.copy_X is not None and not self.copy_X: raise ValueError( "The X matrix is noncontiguous and copy_X = False." @@ -2550,14 +1885,14 @@ def _set_up_and_check_fit_args( # mixed-precision numbers y = np.asarray(y, dtype=X.dtype) - sample_weight = _check_weights( + sample_weight = check_weights( sample_weight, y.shape[0], # type: ignore X.dtype, force_all_finite=force_all_finite, ) - offset = _check_offset(offset, y.shape[0], X.dtype) # type: ignore + offset = check_offset(offset, y.shape[0], X.dtype) # type: ignore # IMPORTANT NOTE: Since we want to minimize # 1/(2*sum(sample_weight)) * deviance + L1 + L2, @@ -2567,985 +1902,212 @@ def _set_up_and_check_fit_args( weights_sum: float = np.sum(sample_weight) # type: ignore sample_weight = sample_weight / weights_sum - # Convert to wrapper matrix types - X = tm.as_tabmat(X) - - self.feature_names_ = X.get_names(type="column", missing_prefix="_col_") # type: ignore - self.term_names_ = X.get_names(type="term", missing_prefix="_col_") - - return X, y, sample_weight, offset, weights_sum, P1, P2 # type: ignore - - -class GeneralizedLinearRegressor(GeneralizedLinearRegressorBase): - """Regression via a Generalized Linear Model (GLM) with penalties. - - GLMs based on a reproductive Exponential Dispersion Model (EDM) aimed at - fitting and predicting the mean of the target ``y`` as ``mu=h(X*w)``. - Therefore, the fit minimizes the following objective function with combined - L1 and L2 priors as regularizer:: - - 1/(2*sum(s)) * deviance(y, h(X*w); s) - + alpha * l1_ratio * ||P1*w||_1 - + 1/2 * alpha * (1 - l1_ratio) * w*P2*w - - with inverse link function ``h`` and ``s=sample_weight``. - Note that, for ``alpha=0`` the unregularized GLM is recovered. - This is not the default behavior (see ``alpha`` parameter description for details). - Additionally, for ``sample_weight=None``, one has ``s_i=1`` and - ``sum(s)=n_samples``. For ``P1=P2='identity'``, the penalty is the elastic net:: - - alpha * l1_ratio * ||w||_1 + 1/2 * alpha * (1 - l1_ratio) * ||w||_2^2. - - If you are interested in controlling the L1 and L2 penalties separately, - keep in mind that this is equivalent to:: - - a * L1 + b * L2, - - where:: - - alpha = a + b and l1_ratio = a / (a + b). - - The parameter ``l1_ratio`` corresponds to alpha in the R package glmnet, - while ``alpha`` corresponds to the lambda parameter in glmnet. - Specifically, ``l1_ratio = 1`` is the lasso penalty. - - Read more in :doc:`background`. - - Parameters - ---------- - alpha : {float, array-like}, optional (default=None) - Constant that multiplies the penalty terms and thus determines the - regularization strength. If ``alpha_search`` is ``False`` (the default), - then ``alpha`` must be a scalar or None (equivalent to ``alpha=0``). - If ``alpha_search`` is ``True``, then ``alpha`` must be an iterable or - ``None``. See ``alpha_search`` to find how the regularization path is - set if ``alpha`` is ``None``. See the notes for the exact mathematical - meaning of this parameter. ``alpha=0`` is equivalent to unpenalized - GLMs. In this case, the design matrix ``X`` must have full column rank - (no collinearities). - - l1_ratio : float, optional (default=0) - The elastic net mixing parameter, with ``0 <= l1_ratio <= 1``. For - ``l1_ratio = 0``, the penalty is an L2 penalty. ``For l1_ratio = 1``, it - is an L1 penalty. For ``0 < l1_ratio < 1``, the penalty is a - combination of L1 and L2. - - P1 : {'identity', array-like, None}, shape (n_features,), optional - (default='identity') - This array controls the strength of the regularization for each coefficient - independently. A high value will lead to higher regularization while a value of - zero will remove the regularization on this parameter. - Note that ``n_features = X.shape[1]``. If ``X`` is a pandas DataFrame - with a categorical dtype and P1 has the same size as the number of columns, - the penalty of the categorical column will be applied to all the levels of - the categorical. - - P2 : {'identity', array-like, sparse matrix, None}, shape (n_features,) \ - or (n_features, n_features), optional (default='identity') - With this option, you can set the P2 matrix in the L2 penalty - ``w*P2*w``. This gives a fine control over this penalty (Tikhonov - regularization). A 2d array is directly used as the square matrix P2. A - 1d array is interpreted as diagonal (square) matrix. The default - ``'identity'`` and ``None`` set the identity matrix, which gives the usual - squared L2-norm. If you just want to exclude certain coefficients, pass a 1d - array filled with 1 and 0 for the coefficients to be excluded. Note that P2 must - be positive semi-definite. If ``X`` is a pandas DataFrame with a categorical - dtype and P2 has the same size as the number of columns, the penalty of the - categorical column will be applied to all the levels of the categorical. Note - that if P2 is two-dimensional, its size needs to be of the same length as the - expanded ``X`` matrix. - - fit_intercept : bool, optional (default=True) - Specifies if a constant (a.k.a. bias or intercept) should be - added to the linear predictor (``X * coef + intercept``). - - family : str or ExponentialDispersionModel, optional (default='normal') - The distributional assumption of the GLM, i.e. the loss function to - minimize. If a string, one of: ``'binomial'``, ``'gamma'``, - ``'gaussian'``, ``'inverse.gaussian'``, ``'normal'``, ``'poisson'``, - ``'tweedie'`` or ``'negative.binomial'``. Note that ``'tweedie'`` sets - the power of the Tweedie distribution to 1.5; to use another value, - specify it in parentheses (e.g., ``'tweedie (1.5)'``). The same applies - for ``'negative.binomial'`` and theta parameter. - - link : {'auto', 'identity', 'log', 'logit', 'cloglog'} oe Link, \ - optional (default='auto') - The link function of the GLM, i.e. mapping from linear - predictor (``X * coef``) to expectation (``mu``). Option ``'auto'`` sets - the link depending on the chosen family as follows: - - - ``'identity'`` for family ``'normal'`` - - ``'log'`` for families ``'poisson'``, ``'gamma'``, - ``'inverse.gaussian'`` and ``'negative.binomial'``. - - ``'logit'`` for family ``'binomial'`` - - solver : {'auto', 'irls-cd', 'irls-ls', 'lbfgs', 'trust-constr'}, \ - optional (default='auto') - Algorithm to use in the optimization problem: - - - ``'auto'``: ``'irls-ls'`` if ``l1_ratio`` is zero and ``'irls-cd'`` otherwise. - - ``'irls-cd'``: Iteratively reweighted least squares with a coordinate - descent inner solver. This can deal with L1 as well as L2 penalties. - Note that in order to avoid unnecessary memory duplication of X in the - ``fit`` method, ``X`` should be directly passed as a - Fortran-contiguous Numpy array or sparse CSC matrix. - - ``'irls-ls'``: Iteratively reweighted least squares with a least - squares inner solver. This algorithm cannot deal with L1 penalties. - - ``'lbfgs'``: Scipy's L-BFGS-B optimizer. It cannot deal with L1 - penalties. - - ``'trust-constr'``: Calls - ``scipy.optimize.minimize(method='trust-constr')``. It cannot deal - with L1 penalties. This solver can optimize problems with inequality - constraints, passed via ``A_ineq`` and ``b_ineq``. It will be selected - automatically when inequality constraints are set and - ``solver='auto'``. Note that using this method can lead to - significantly increased runtimes by a factor of ten or higher. - - max_iter : int, optional (default=100) - The maximal number of iterations for solver algorithms. - - max_inner_iter: int, optional (default=100000) - The maximal number of iterations for the inner solver in the IRLS-CD - algorithm. This parameter is only used when ``solver='irls-cd'``. - - gradient_tol : float, optional (default=None) - Stopping criterion. If ``None``, solver-specific defaults will be used. - The default value for most solvers is ``1e-4``, except for - ``'trust-constr'``, which requires more conservative convergence - settings and has a default value of ``1e-8``. - - For the IRLS-LS, L-BFGS and trust-constr solvers, the iteration - will stop when ``max{|g_i|, i = 1, ..., n} <= tol``, where ``g_i`` is - the ``i``-th component of the gradient (derivative) of the objective - function. For the CD solver, convergence is reached when - ``sum_i(|minimum norm of g_i|)``, where ``g_i`` is the subgradient of - the objective and the minimum norm of ``g_i`` is the element of the - subgradient with the smallest L2 norm. - - If you wish to only use a step-size tolerance, set ``gradient_tol`` - to a very small number. - - step_size_tol: float, optional (default=None) - Alternative stopping criterion. For the IRLS-LS and IRLS-CD solvers, the - iteration will stop when the L2 norm of the step size is less than - ``step_size_tol``. This stopping criterion is disabled when - ``step_size_tol`` is ``None``. - - hessian_approx: float, optional (default=0.0) - The threshold below which data matrix rows will be ignored for updating - the Hessian. See the algorithm documentation for the IRLS algorithm - for further details. - - warm_start : bool, optional (default=False) - Whether to reuse the solution of the previous call to ``fit`` - as initialization for ``coef_`` and ``intercept_`` (supersedes - ``start_params``). If ``False`` or if the attribute ``coef_`` does not - exist (first call to ``fit``), ``start_params`` sets the start values - for ``coef_`` and ``intercept_``. - - alpha_search : bool, optional (default=False) - Whether to search along the regularization path for the best alpha. - When set to ``True``, ``alpha`` should either be ``None`` or an - iterable. To determine the regularization path, the following sequence - is used: - - 1. If ``alpha`` is an iterable, use it directly. All other parameters - governing the regularization path are ignored. - 2. If ``min_alpha`` is set, create a path from ``min_alpha`` to the - lowest alpha such that all coefficients are zero. - 3. If ``min_alpha_ratio`` is set, create a path where the ratio of - ``min_alpha / max_alpha = min_alpha_ratio``. - 4. If none of the above parameters are set, use a ``min_alpha_ratio`` of - ``1e-6``. - - alphas : DEPRECATED. Use ``alpha`` instead. - - n_alphas : int, optional (default=100) - Number of alphas along the regularization path - - min_alpha_ratio : float, optional (default=None) - Length of the path. ``min_alpha_ratio=1e-6`` means that - ``min_alpha / max_alpha = 1e-6``. If ``None``, ``1e-6`` is used. - - min_alpha : float, optional (default=None) - Minimum alpha to estimate the model with. The grid will then be created - over ``[max_alpha, min_alpha]``. - - start_params : array-like, shape (n_features*,), optional (default=None) - Relevant only if ``warm_start`` is ``False`` or if ``fit`` is called - for the first time (so that ``self.coef_`` does not exist yet). If - ``None``, all coefficients are set to zero and the start value for the - intercept is the weighted average of ``y`` (If ``fit_intercept`` is - ``True``). If an array, used directly as start values; if - ``fit_intercept`` is ``True``, its first element is assumed to be the - start value for the ``intercept_``. Note that - ``n_features* = X.shape[1] + fit_intercept``, i.e. it includes the - intercept. - - selection : str, optional (default='cyclic') - For the CD solver 'cd', the coordinates (features) can be updated in - either cyclic or random order. If set to ``'random'``, a random - coefficient is updated every iteration rather than looping over features - sequentially in the same order, which often leads to significantly - faster convergence, especially when ``gradient_tol`` is higher than - ``1e-4``. - - random_state : int or RandomState, optional (default=None) - The seed of the pseudo random number generator that selects a random - feature to be updated for the CD solver. If an integer, ``random_state`` - is the seed used by the random number generator; if a - :class:`RandomState` instance, ``random_state`` is the random number - generator; if ``None``, the random number generator is the - :class:`RandomState` instance used by ``np.random``. Used when - ``selection`` is ``'random'``. - - copy_X : bool, optional (default=None) - Whether to copy ``X``. Since ``X`` is never modified by - :class:`GeneralizedLinearRegressor`, this is unlikely to be needed; this - option exists mainly for compatibility with other scikit-learn - estimators. If ``False``, ``X`` will not be copied and there will be an - error if you pass an ``X`` in the wrong format, such as providing - integer ``X`` and float ``y``. If ``None``, ``X`` will not be copied - unless it is in the wrong format. - - check_input : bool, optional (default=True) - Whether to bypass several checks on input: ``y`` values in range of - ``family``, ``sample_weight`` non-negative, ``P2`` positive - semi-definite. Don't use this parameter unless you know what you are - doing. - - verbose : int, optional (default=0) - For the IRLS solver, any positive number will result in a pretty - progress bar showing convergence. This features requires having the - tqdm package installed. For the L-BFGS and ``'trust-constr'`` solvers, - set ``verbose`` to any positive number for verbosity. - - scale_predictors: bool, optional (default=False) - If ``True``, scale all predictors to have standard deviation one. - Should be set to ``True`` if ``alpha > 0`` and if you want coefficients - to be penalized equally. - - Reported coefficient estimates are always at the original scale. - - Advanced developer note: Internally, predictors are always rescaled for - computational reasons, but this only affects results if - ``scale_predictors`` is ``True``. - - lower_bounds : array-like, shape (n_features,), optional (default=None) - Set a lower bound for the coefficients. Setting bounds forces the use - of the coordinate descent solver (``'irls-cd'``). - - upper_bounds : array-like, shape=(n_features,), optional (default=None) - See ``lower_bounds``. - - A_ineq : array-like, shape=(n_constraints, n_features), optional (default=None) - Constraint matrix for linear inequality constraints of the form - ``A_ineq w <= b_ineq``. Setting inequality constraints forces the use - of the local gradient-based solver ``'trust-constr'``, which may - increase runtime significantly. Note that the constraints only apply - to coefficients related to features in ``X``. If you want to constrain - the intercept, add it to the feature matrix ``X`` manually and set - ``fit_intercept==False``. - - b_ineq : array-like, shape=(n_constraints,), optional (default=None) - Constraint vector for linear inequality constraints of the form - ``A_ineq w <= b_ineq``. Refer to the documentation of ``A_ineq`` for - details. - - drop_first : bool, optional (default = False) - If ``True``, drop the first column when encoding categorical variables. - Set this to True when ``alpha=0`` and ``solver='auto'`` to prevent an error - due to a singular feature matrix. In the case of using a formula with - interactions, setting this argument to ``True`` ensures structural - full-rankness (it is equivalent to ``ensure_full_rank`` in formulaic and - tabmat). - - robust : bool, optional (default = False) - If true, then robust standard errors are computed by default. - - expected_information : bool, optional (default = False) - If true, then the expected information matrix is computed by default. - Only relevant when computing robust standard errors. - - formula : formulaic.FormulaSpec - A formula accepted by formulaic. It can either be a one-sided formula, in - which case ``y`` must be specified in ``fit``, or a two-sided formula, in - which case ``y`` must be ``None``. - - interaction_separator: str, default=":" - The separator between the names of interacted variables. - - categorical_format : str, optional, default='{name}[{category}]' - Format string for categorical features. The format string should - contain the placeholder ``{name}`` for the feature name and - ``{category}`` for the category name. Only used if ``X`` is a pandas - DataFrame. - - cat_missing_method: str {'fail'|'zero'|'convert'}, default='fail' - How to handle missing values in categorical columns. Only used if ``X`` - is a pandas data frame. - - if 'fail', raise an error if there are missing values - - if 'zero', missing values will represent all-zero indicator columns. - - if 'convert', missing values will be converted to the ``cat_missing_name`` - category. - - cat_missing_name: str, default='(MISSING)' - Name of the category to which missing values will be converted if - ``cat_missing_method='convert'``. Only used if ``X`` is a pandas data frame. - - Attributes - ---------- - coef_ : numpy.array, shape (n_features,) - Estimated coefficients for the linear predictor (X*coef_+intercept_) in - the GLM. - - intercept_ : float - Intercept (a.k.a. bias) added to linear predictor. - - n_iter_ : int - Actual number of iterations used in solver. - - col_means_: array, shape (n_features,) - The means of the columns of the design matrix ``X``. - - col_stds_: array, shape (n_features,) - The standard deviations of the columns of the design matrix ``X``. - - Notes - ----- - The fit itself does not need outcomes to be from an EDM, but only assumes - the first two moments to be - :math:`\\mu_i \\equiv \\mathrm{E}(y_i) = h(x_i' w)` and - :math:`\\mathrm{var}(y_i) = (\\phi / s_i) v(\\mu_i)`. The unit - variance function :math:`v(\\mu_i)` is a property of and given by the - specific EDM; see :doc:`background`. - - The parameters :math:`w` (``coef_`` and ``intercept_``) are estimated by - minimizing the deviance plus penalty term, which is equivalent to - (penalized) maximum likelihood estimation. - - If the target ``y`` is a ratio, appropriate sample weights ``s`` should be - provided. As an example, consider Poisson distributed counts ``z`` - (integers) and weights ``s = exposure`` (time, money, persons years, ...). - Then you fit ``y ≡ z/s``, i.e. - ``GeneralizedLinearModel(family='poisson').fit(X, y, sample_weight=s)``. The - weights are necessary for the right (finite sample) mean. Consider - :math:`\\bar{y} = \\sum_i s_i y_i / \\sum_i s_i`: in this case, one might - say that :math:`y` follows a 'scaled' Poisson distribution. The same holds - for other distributions. - - References - ---------- - For the coordinate descent implementation: - * Guo-Xun Yuan, Chia-Hua Ho, Chih-Jen Lin - An Improved GLMNET for L1-regularized Logistic Regression, - Journal of Machine Learning Research 13 (2012) 1999-2030 - https://www.csie.ntu.edu.tw/~cjlin/papers/l1_glmnet/long-glmnet.pdf - """ - - def __init__( - self, - *, - alpha=None, - l1_ratio=0, - P1: Optional[Union[str, np.ndarray]] = "identity", - P2: Optional[Union[str, np.ndarray, sparse.spmatrix]] = "identity", - fit_intercept=True, - family: Union[str, ExponentialDispersionModel] = "normal", - link: Union[str, Link] = "auto", - solver: str = "auto", - max_iter=100, - max_inner_iter=100000, - gradient_tol: Optional[float] = None, - step_size_tol: Optional[float] = None, - hessian_approx: float = 0.0, - warm_start: bool = False, - alpha_search: bool = False, - alphas: Optional[np.ndarray] = None, - n_alphas: int = 100, - min_alpha_ratio: Optional[float] = None, - min_alpha: Optional[float] = None, - start_params: Optional[np.ndarray] = None, - selection: str = "cyclic", - random_state=None, - copy_X: Optional[bool] = None, - check_input: bool = True, - verbose=0, - scale_predictors: bool = False, - lower_bounds: Optional[np.ndarray] = None, - upper_bounds: Optional[np.ndarray] = None, - A_ineq: Optional[np.ndarray] = None, - b_ineq: Optional[np.ndarray] = None, - force_all_finite: bool = True, - drop_first: bool = False, - robust: bool = True, - expected_information: bool = False, - formula: Optional[FormulaSpec] = None, - interaction_separator: str = ":", - categorical_format: str = "{name}[{category}]", - cat_missing_method: str = "fail", - cat_missing_name: str = "(MISSING)", - ): - self.alphas = alphas - self.alpha = alpha - super().__init__( - l1_ratio=l1_ratio, - P1=P1, - P2=P2, - fit_intercept=fit_intercept, - family=family, - link=link, - solver=solver, - max_iter=max_iter, - max_inner_iter=max_inner_iter, - gradient_tol=gradient_tol, - step_size_tol=step_size_tol, - hessian_approx=hessian_approx, - warm_start=warm_start, - alpha_search=alpha_search, - n_alphas=n_alphas, - min_alpha=min_alpha, - min_alpha_ratio=min_alpha_ratio, - start_params=start_params, - selection=selection, - random_state=random_state, - copy_X=copy_X, - check_input=check_input, - verbose=verbose, - scale_predictors=scale_predictors, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, - A_ineq=A_ineq, - b_ineq=b_ineq, - force_all_finite=force_all_finite, - drop_first=drop_first, - robust=robust, - expected_information=expected_information, - formula=formula, - interaction_separator=interaction_separator, - categorical_format=categorical_format, - cat_missing_method=cat_missing_method, - cat_missing_name=cat_missing_name, - ) - - def _validate_hyperparameters(self) -> None: - if self.alpha_search: - if not isinstance(self.alpha, Iterable) and self.alpha is not None: - raise ValueError( - "`alpha` should be an Iterable or None when `alpha_search`" - " is True" - ) - if self.alpha is not None and ( - (np.asarray(self.alpha) < 0).any() - or not np.issubdtype(np.asarray(self.alpha).dtype, np.number) - ): - raise ValueError("`alpha` must contain only non-negative numbers") - if not self.alpha_search: - if not np.isscalar(self.alpha) and self.alpha is not None: - raise ValueError( - "`alpha` should be a scalar or None when `alpha_search`" " is False" - ) - if self.alpha is not None and ( - not isinstance(self.alpha, (int, float)) or self.alpha < 0 - ): - raise ValueError( - "Penalty term must be a non-negative number;" - f" got (alpha={self.alpha})" # type: ignore - ) - - if ( - not np.isscalar(self.l1_ratio) - # check for numeric, i.e. not a string - or not np.issubdtype(np.asarray(self.l1_ratio).dtype, np.number) - or self.l1_ratio < 0 # type: ignore - or self.l1_ratio > 1 # type: ignore - ): - raise ValueError( - "l1_ratio must be a number in interval [0, 1];" - f" got (l1_ratio={self.l1_ratio})" - ) - super()._validate_hyperparameters() - - def fit( - self, - X: ArrayLike, - y: Optional[ArrayLike] = None, - sample_weight: Optional[ArrayLike] = None, - offset: Optional[ArrayLike] = None, - *, - store_covariance_matrix: bool = False, - clusters: Optional[np.ndarray] = None, - # TODO: take out weights_sum (or use it properly) - weights_sum: Optional[float] = None, - context: Optional[Union[int, Mapping[str, Any]]] = None, - ): - """Fit a Generalized Linear Model. - - Parameters - ---------- - X : {array-like, sparse matrix}, shape (n_samples, n_features) - Training data. Note that a ``float32`` matrix is acceptable and will - result in the entire algorithm being run in 32-bit precision. - However, for problems that are poorly conditioned, this might result - in poor convergence or flawed parameter estimates. If a Pandas data - frame is provided, it may contain categorical columns. In that case, - a separate coefficient will be estimated for each category. No - category is omitted. This means that some regularization is required - to fit models with an intercept or models with several categorical - columns. - - y : array-like, shape (n_samples,) - Target values. - - sample_weight : array-like, shape (n_samples,), optional (default=None) - Individual weights w_i for each sample. Note that, for an - Exponential Dispersion Model (EDM), one has - :math:`\\mathrm{var}(y_i) = \\phi \\times v(mu) / w_i`. - If :math:`y_i \\sim EDM(\\mu, \\phi / w_i)`, then - :math:`\\sum w_i y_i / \\sum w_i \\sim EDM(\\mu, \\phi / \\sum w_i)`, - i.e. the mean of :math:`y` is a weighted average with weights equal - to ``sample_weight``. - - offset: array-like, shape (n_samples,), optional (default=None) - Added to linear predictor. An offset of 3 will increase expected - ``y`` by 3 if the link is linear and will multiply expected ``y`` by - 3 if the link is logarithmic. - - store_covariance_matrix : bool, optional (default=False) - Whether to estimate and store the covariance matrix of the parameter - estimates. If ``True``, the covariance matrix will be available in the - ``covariance_matrix_`` attribute after fitting. - - clusters : array-like, optional, default=None - Array with cluster membership. Clustered standard errors are - computed if clusters is not None. - - context : Optional[Union[int, Mapping[str, Any]]], default=None - The context to add to the evaluation context of the formula with, - e.g., custom transforms. If an integer, the context is taken from - the stack frame of the caller at the given depth. Otherwise, a - mapping from variable names to values is expected. By default, - no context is added. Set ``context=0`` to make the calling scope - available. - - weights_sum: float, optional (default=None) - - Returns - ------- - self - """ - - self._validate_hyperparameters() - - captured_context = capture_context( - context + 1 if isinstance(context, int) else context - ) - - # NOTE: This function checks if all the entries in X and y are - # finite. That can be expensive. But probably worthwhile. - ( - X, - y, - sample_weight, - offset, - weights_sum, - P1, - P2, - ) = self._set_up_and_check_fit_args( - X, - y, - sample_weight, - offset, - force_all_finite=self.force_all_finite, - context=captured_context, - ) - assert isinstance(X, tm.MatrixBase) - assert isinstance(y, np.ndarray) - - self._set_up_for_fit(y) - - # 1.3 arguments to take special care ################################## - # P1, P2, start_params - stype = ["csc"] if self._solver == "irls-cd" else ["csc", "csr"] - P1_no_alpha = setup_p1(P1, X, X.dtype, 1, self.l1_ratio) - P2_no_alpha = setup_p2(P2, X, stype, X.dtype, 1, self.l1_ratio) - - lower_bounds = check_bounds(self.lower_bounds, X.shape[1], X.dtype) - upper_bounds = check_bounds(self.upper_bounds, X.shape[1], X.dtype) - - A_ineq, b_ineq = check_inequality_constraints( - self.A_ineq, self.b_ineq, n_features=X.shape[1], dtype=X.dtype - ) - - if (lower_bounds is not None) and (upper_bounds is not None): - if np.any(lower_bounds > upper_bounds): - raise ValueError("Upper bounds must be higher than lower bounds.") - - # 1.4 additional validations ########################################## - if self.check_input: - # check if P2 is positive semidefinite - if not isinstance(self.P2, str): # self.P2 != 'identity' - if not is_pos_semidef(P2_no_alpha): - if P2_no_alpha.ndim == 1 or P2_no_alpha.shape[0] == 1: - error = "1d array P2 must not have negative values." - else: - error = "P2 must be positive semi-definite." - raise ValueError(error) - # TODO: if alpha=0 check that X is not rank deficient - # TODO: what else to check? - - ####################################################################### - # 2c. potentially rescale predictors - ####################################################################### - - ( - X, - self.col_means_, - self.col_stds_, - lower_bounds, - upper_bounds, - A_ineq, - P1_no_alpha, - P2_no_alpha, - ) = _standardize( - X, - sample_weight, - self._center_predictors, - self.scale_predictors, - lower_bounds, - upper_bounds, - A_ineq, - P1_no_alpha, - P2_no_alpha, - ) - - ####################################################################### - # 3. initialization of coef = (intercept_, coef_) # - ####################################################################### - - coef = self._get_start_coef( - X, - y, - sample_weight, - offset, - self.col_means_, - self.col_stds_, - dtype=[np.float64, np.float32], - ) - - ####################################################################### - # 4. fit # - ####################################################################### - if self.alpha_search: - if self.alphas is not None: - warnings.warn( - "alphas is deprecated. Use alpha instead.", DeprecationWarning - ) - self._alphas = self.alphas - elif self.alpha is None: - self._alphas = self._get_alpha_path( - P1_no_alpha=P1_no_alpha, X=X, y=y, w=sample_weight, offset=offset - ) - else: - self._alphas = self.alpha - if self.min_alpha is not None or self.min_alpha_ratio is not None: - warnings.warn( - "`alpha` is set. Ignoring `min_alpha` and `min_alpha_ratio`." - ) - - coef = self._solve_regularization_path( - X=X, - y=y, - sample_weight=sample_weight, - P2_no_alpha=P2_no_alpha, - P1_no_alpha=P1_no_alpha, - alphas=self._alphas, - coef=coef, - offset=offset, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, - A_ineq=A_ineq, - b_ineq=b_ineq, - ) - - # intercept_ and coef_ return the last estimated alpha - if self.fit_intercept: - self.intercept_path_, self.coef_path_ = _unstandardize( - self.col_means_, self.col_stds_, coef[:, 0], coef[:, 1:] - ) - self.intercept_ = self.intercept_path_[-1] # type: ignore - self.coef_ = self.coef_path_[-1] - else: - # set intercept to zero as the other linear models do - self.intercept_path_, self.coef_path_ = _unstandardize( - self.col_means_, self.col_stds_, np.zeros(coef.shape[0]), coef - ) - self.intercept_ = 0.0 - self.coef_ = self.coef_path_[-1] - else: - if self.alpha is None: - _alpha = 0.0 - else: - _alpha = self.alpha - if _alpha > 0 and self.l1_ratio > 0 and self._solver != "irls-cd": - raise ValueError( - f"The chosen solver (solver={self._solver}) can't deal " - "with L1 penalties, which are included with " - f"(alpha={_alpha}) and (l1_ratio={self.l1_ratio})." - ) - coef = self._solve( - X=X, - y=y, - sample_weight=sample_weight, - P2=P2_no_alpha * _alpha, - P1=P1_no_alpha * _alpha, - coef=coef, - offset=offset, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, - A_ineq=A_ineq, - b_ineq=b_ineq, - ) - - if self.fit_intercept: - self.intercept_, self.coef_ = _unstandardize( - self.col_means_, self.col_stds_, coef[0], coef[1:] - ) - else: - # set intercept to zero as the other linear models do - self.intercept_, self.coef_ = _unstandardize( - self.col_means_, self.col_stds_, 0.0, coef - ) - - self.covariance_matrix_ = None - if store_covariance_matrix: - self.covariance_matrix( - X=X.unstandardize(), - y=y, - offset=offset, - sample_weight=sample_weight * weights_sum, - robust=getattr(self, "robust", True), - clusters=clusters, - expected_information=getattr(self, "expected_information", False), - store_covariance_matrix=True, - skip_checks=True, - ) - - return self - - def _compute_information_criteria( - self, - X: ShapedArrayLike, - y: ShapedArrayLike, - sample_weight: Optional[ArrayLike] = None, - context: Optional[Mapping[str, Any]] = None, - ): - """ - Computes and stores the model's degrees of freedom, the 'aic', 'aicc' - and 'bic' information criteria. - - The model's degrees of freedom are used to calculate the effective - number of parameters. This uses the claim by [2] and [3] that, for - L1 regularisation, the number of non-zero parameters in the trained model - is an unbiased approximator of the degrees of freedom of the model. Note - that this might not hold true for L2 regularisation and thus we raise a - warning for this case. + # Convert to wrapper matrix types + X = tm.as_tabmat(X) - References - ---------- - [1] Burnham KP, Anderson KR (2002). Model Selection and Multimodel - Inference; Springer New York. - [2] Zou, H., Hastie, T. and Tibshirani, R., (2007). On the “degrees of - freedom” of the lasso; The Annals of Statistics. - [3] Park, M.Y., 2006. Generalized linear models with regularization; - Stanford Universty. - """ - if not hasattr(self.family_instance, "log_likelihood"): - raise NotImplementedError( - "The family instance does not define a `log_likelihood` method, so " - "information criteria cannot be computed. Compatible families include " - "the binomial, negative binomial and Tweedie (power<=2 or power=3)." - ) + self.feature_names_ = X.get_names(type="column", missing_prefix="_col_") # type: ignore + self.term_names_ = X.get_names(type="term", missing_prefix="_col_") - ddof = np.sum(np.abs(self.coef_) > np.finfo(self.coef_.dtype).eps) # type: ignore - k_params = ddof + self.fit_intercept - nobs = X.shape[0] + return X, y, sample_weight, offset, weights_sum, P1, P2 # type: ignore - if nobs != self._num_obs: - raise ValueError( - "The same dataset that was used for training should also be used for " - "the computation of information criteria." - ) - mu = self.predict(X, context=context) - ll = self.family_instance.log_likelihood(y, mu, sample_weight=sample_weight) +def get_family( + family: Union[str, ExponentialDispersionModel], +) -> ExponentialDispersionModel: + if isinstance(family, ExponentialDispersionModel): + return family - aic = -2 * ll + 2 * k_params - bic = -2 * ll + np.log(nobs) * k_params + name_to_dist = { + "binomial": BinomialDistribution(), + "gamma": GammaDistribution(), + "gaussian": NormalDistribution(), + "inverse.gaussian": InverseGaussianDistribution(), + "normal": NormalDistribution(), + "poisson": PoissonDistribution(), + "tweedie": TweedieDistribution(1.5), + "negative.binomial": NegativeBinomialDistribution(1.0), + } - if nobs > k_params + 1: - aicc = aic + 2 * k_params * (k_params + 1) / (nobs - k_params - 1) - else: - aicc = None + if family in name_to_dist: + return name_to_dist[family] - self._info_criteria = {"aic": aic, "aicc": aicc, "bic": bic} + custom_tweedie = re.search(r"tweedie\s?\((.+)\)", family) - return True + if custom_tweedie: + return TweedieDistribution(float(custom_tweedie.group(1))) - def aic( - self, - X: ArrayLike, - y: ArrayLike, - sample_weight: Optional[ArrayLike] = None, - *, - context: Optional[Union[int, Mapping[str, Any]]] = None, - ): - """ - Akaike's information criteria. Computed as: - :math:`-2\\log\\hat{\\mathcal{L}} + 2\\hat{k}` where - :math:`\\hat{\\mathcal{L}}` is the maximum likelihood estimate of the - model, and :math:`\\hat{k}` is the effective number of parameters. See - `_compute_information_criteria` for more information on the computation - of :math:`\\hat{k}`. + custom_negative_binomial = re.search(r"negative.binomial\s?\((.+)\)", family) - Parameters - ---------- - X : {array-like, sparse matrix}, shape (n_samples, n_features) - Same data as used in 'fit' + if custom_negative_binomial: + return NegativeBinomialDistribution(float(custom_negative_binomial.group(1))) - y : array-like, shape (n_samples,) - Same data as used in 'fit' + raise ValueError( + "The family must be an instance of class ExponentialDispersionModel or an " + f"element of {sorted(name_to_dist.keys())}; got (family={family})." + ) - sample_weight : array-like, shape (n_samples,), optional (default=None) - Same data as used in 'fit' - context : Optional[Union[int, Mapping[str, Any]]], default=None - The context to add to the evaluation context of the formula with, - e.g., custom transforms. If an integer, the context is taken from - the stack frame of the caller at the given depth. Otherwise, a - mapping from variable names to values is expected. By default, - no context is added. Set ``context=0`` to make the calling scope - available. - """ - captured_context = capture_context( - context + 1 if isinstance(context, int) else context - ) - return self._get_info_criteria( - "aic", X, y, sample_weight, context=captured_context +def get_link(link: Union[str, Link], family: ExponentialDispersionModel) -> Link: + """ + For the Tweedie distribution, this code follows actuarial best practices + regarding link functions. Note that these links are sometimes not canonical: + - identity for normal (``p = 0``); + - no convention for ``p < 0``, so let's leave it as identity; + - log otherwise. + """ + if isinstance(link, Link): + return link + + if (link is None) or (link == "auto"): + if tweedie_representation := family.to_tweedie(safe=False): + if tweedie_representation.power <= 0: + return IdentityLink() + return LogLink() + if isinstance(family, GeneralizedHyperbolicSecant): + return IdentityLink() + if isinstance(family, BinomialDistribution): + return LogitLink() + if isinstance(family, NegativeBinomialDistribution): + return LogLink() + raise ValueError( + "No default link known for the specified distribution family. " + "Please set link manually, i.e. not to 'auto'. " + f"Got (link='auto', family={family.__class__.__name__})." ) - def aicc( - self, - X: ArrayLike, - y: ArrayLike, - sample_weight: Optional[ArrayLike] = None, - *, - context: Optional[Union[int, Mapping[str, Any]]] = None, - ): - """ - Second-order Akaike's information criteria (or small sample AIC). - Computed as: - :math:`-2\\log\\hat{\\mathcal{L}} + 2\\hat{k} + \\frac{2k(k+1)}{n-k-1}` - where :math:`\\hat{\\mathcal{L}}` is the maximum likelihood estimate of - the model, :math:`n` is the number of training instances, and - :math:`\\hat{k}` is the effective number of parameters. See - `_compute_information_criteria` for more information on the computation - of :math:`\\hat{k}`. + mapping = { + "cloglog": CloglogLink(), + "identity": IdentityLink(), + "log": LogLink(), + "logit": LogitLink(), + "tweedie": TweedieLink(1.5), + } - Parameters - ---------- - X : {array-like, sparse matrix}, shape (n_samples, n_features) - Same data as used in 'fit' + if link in mapping: + return mapping[link] + if custom_tweedie := re.search(r"tweedie\s?\((.+)\)", link): + return TweedieLink(float(custom_tweedie.group(1))) - y : array-like, shape (n_samples,) - Same data as used in 'fit' + raise ValueError( + "The link must be an instance of class Link or an element of " + "['auto', 'identity', 'log', 'logit', 'cloglog', 'tweedie']; " + f"got (link={link})." + ) - sample_weight : array-like, shape (n_samples,), optional (default=None) - Same data as used in 'fit' - context : Optional[Union[int, Mapping[str, Any]]], default=None - The context to add to the evaluation context of the formula with, - e.g., custom transforms. If an integer, the context is taken from - the stack frame of the caller at the given depth. Otherwise, a - mapping from variable names to values is expected. By default, - no context is added. Set ``context=0`` to make the calling scope - available. - """ - captured_context = capture_context( - context + 1 if isinstance(context, int) else context - ) - aicc = self._get_info_criteria( - "aicc", X, y, sample_weight, context=captured_context - ) - if not aicc: +def setup_p1( + P1: Optional[Union[str, np.ndarray]], + X: Union[tm.MatrixBase, tm.StandardizedMatrix], + dtype, + alpha: float, + l1_ratio: float, +) -> np.ndarray: + if not isinstance(X, (tm.MatrixBase, tm.StandardizedMatrix)): + raise TypeError + + n_features = X.shape[1] + + if isinstance(P1, str): + if P1 != "identity": + raise ValueError(f"P1 must be either 'identity' or an array; got {P1}.") + P1 = np.ones(n_features, dtype=dtype) + elif P1 is None: + P1 = np.ones(n_features, dtype=dtype) + else: + P1 = np.atleast_1d(P1) + try: + P1 = P1.astype(dtype, casting="safe", copy=False) # type: ignore + except TypeError as e: + raise TypeError( + "The given P1 cannot be converted to a numeric array; " + f"got (P1.dtype={P1.dtype})." # type: ignore + ) from e + if (P1.ndim != 1) or (P1.shape[0] != n_features): # type: ignore raise ValueError( - "Model degrees of freedom should be more than training datapoints." + "P1 must be either 'identity' or a 1d array with the length of " + "X.shape[1] (either before or after categorical expansion); " + f"got (P1.shape[0]={P1.shape[0]})." # type: ignore ) - return aicc - - def bic( - self, - X: ArrayLike, - y: ArrayLike, - sample_weight: Optional[ArrayLike] = None, - *, - context: Optional[Union[int, Mapping[str, Any]]] = None, - ): - """ - Bayesian information criterion. Computed as: - :math:`-2\\log\\hat{\\mathcal{L}} + k\\log(n)` where - :math:`\\hat{\\mathcal{L}}` is the maximum likelihood estimate of the - model, :math:`n` is the number of training instances, and - :math:`\\hat{k}` is the effective number of parameters. See - `_compute_information_criteria` for more information on the computation - of :math:`\\hat{k}`. - Parameters - ---------- - X : {array-like, sparse matrix}, shape (n_samples, n_features) - Same data as used in 'fit' + # P1 and P2 are now for sure copies + P1 = alpha * l1_ratio * P1 # type: ignore - y : array-like, shape (n_samples,) - Same data as used in 'fit' + return typing.cast(np.ndarray, P1).astype(dtype) - sample_weight : array-like, shape (n_samples,), optional (default=None) - Same data as used in 'fit' - context : Optional[Union[int, Mapping[str, Any]]], default=None - The context to add to the evaluation context of the formula with, - e.g., custom transforms. If an integer, the context is taken from - the stack frame of the caller at the given depth. Otherwise, a - mapping from variable names to values is expected. By default, - no context is added. Set ``context=0`` to make the calling scope - available. - """ - captured_context = capture_context( - context + 1 if isinstance(context, int) else context - ) - return self._get_info_criteria( - "bic", X, y, sample_weight, context=captured_context - ) +def setup_p2( + P2: Optional[Union[str, np.ndarray, sparse.spmatrix]], + X: Union[tm.MatrixBase, tm.StandardizedMatrix], + stype, + dtype, + alpha: float, + l1_ratio: float, +) -> Union[np.ndarray, sparse.spmatrix]: + if not isinstance(X, (tm.MatrixBase, tm.StandardizedMatrix)): + raise TypeError - def _get_info_criteria( - self, - crit: str, - X: ArrayLike, - y: ArrayLike, - sample_weight: Optional[ArrayLike] = None, - context: Optional[Mapping[str, Any]] = None, - ): - check_is_fitted(self, "coef_") + n_features = X.shape[1] - if not hasattr(self, "_info_criteria"): - self._compute_information_criteria(X, y, sample_weight, context=context) + def _setup_sparse_p2(P2): + return (sparse.dia_matrix((P2, 0), shape=(n_features, n_features))).tocsc() - if ( - self.alpha is None or (self.alpha is not None and self.alpha > 0) - ) and self.l1_ratio < 1.0: - warnings.warn( - "There is no general definition for the model's degrees of " - + f"freedom under L2 (ridge) regularisation. The {crit} " - + "might not be well defined in these cases." + if isinstance(P2, str): + if P2 != "identity": + raise ValueError(f"P2 must be either 'identity' or an array. Got {P2}.") + if sparse.issparse(X): # if X is sparse, make P2 sparse, too + P2 = _setup_sparse_p2(np.ones(n_features, dtype=dtype)) + else: + P2 = np.ones(n_features, dtype=dtype) + elif P2 is None: + if sparse.issparse(X): # if X is sparse, make P2 sparse, too + P2 = _setup_sparse_p2(np.ones(n_features, dtype=dtype)) + else: + P2 = np.ones(n_features, dtype=dtype) + else: + P2 = skl.utils.check_array( + P2, copy=True, accept_sparse=stype, dtype=dtype, ensure_2d=False + ) + P2 = typing.cast(np.ndarray, P2) + if P2.ndim == 1: + P2 = np.asarray(P2) + if P2.shape[0] != n_features: + raise ValueError( + "P2 should be a 1d array of shape X.shape[1] (either before or " + "after categorical expansion); " + f"got (P2.shape={P2.shape})." + ) + if sparse.issparse(X): + P2 = _setup_sparse_p2(P2) + elif P2.ndim == 2 and P2.shape[0] == P2.shape[1] and P2.shape[0] == n_features: + if sparse.issparse(X): + P2 = sparse.csc_matrix(P2) + else: + raise ValueError( + "P2 must be either None or an array of shape (n_features, n_features) " + f"with n_features=X.shape[1]; got (P2.shape={P2.shape}); " + f"needed ({n_features}, {n_features})." ) - return self._info_criteria[crit] + # P1 and P2 are now for sure copies + P2 = alpha * (1 - l1_ratio) * P2 + # one only ever needs the symmetrized L2 penalty matrix 1/2 (P2 + P2') + # reason: w' P2 w = (w' P2 w)', i.e. it is symmetric + if P2.ndim == 2: + if sparse.issparse(P2): + if sparse.isspmatrix_csc(P2): + P2 = 0.5 * (P2 + P2.transpose()).tocsc() + else: + P2 = 0.5 * (P2 + P2.transpose()).tocsr() + else: + P2 = 0.5 * (P2 + P2.T) + return P2 + + +def _group_sum(groups: np.ndarray, data: tm.MatrixBase): + """Sum over groups.""" + ngroups = len(np.unique(groups)) + out = np.empty((ngroups, data.shape[1])) + eye_n = sps.eye(ngroups, format="csc")[:, groups] + for i in range(data.shape[1]): + out[:, i] = safe_toarray(eye_n @ data.getcol(i).unpack()).ravel() + return out diff --git a/src/glum/_glm_cv.py b/src/glum/_glm_cv.py index 9572e23a9..f4e15f8ce 100644 --- a/src/glum/_glm_cv.py +++ b/src/glum/_glm_cv.py @@ -2,25 +2,19 @@ from collections.abc import Mapping from typing import Any, Optional, Union +import formulaic +import joblib import numpy as np -from formulaic import FormulaSpec -from formulaic.utils.context import capture_context -from joblib import Parallel, delayed -from sklearn.model_selection._split import check_cv +import sklearn as skl from ._distribution import ExponentialDispersionModel -from ._glm import ( - ArrayLike, - GeneralizedLinearRegressorBase, - _standardize, - _unstandardize, - check_bounds, - is_pos_semidef, - setup_p1, - setup_p2, -) +from ._formula import capture_context +from ._glm import GeneralizedLinearRegressorBase, setup_p1, setup_p2 +from ._linalg import _safe_lin_pred, is_pos_semidef from ._link import Link, LogLink -from ._util import _safe_lin_pred +from ._typing import ArrayLike +from ._utils import standardize, unstandardize +from ._validation import check_bounds class GeneralizedLinearRegressorCV(GeneralizedLinearRegressorBase): @@ -368,7 +362,7 @@ def __init__( drop_first: bool = False, robust: bool = True, expected_information: bool = False, - formula: Optional[FormulaSpec] = None, + formula: Optional[formulaic.FormulaSpec] = None, interaction_separator: str = ":", categorical_format: str = "{name}[{category}]", cat_missing_method: str = "fail", @@ -494,10 +488,6 @@ def fit( """ self._validate_hyperparameters() - captured_context = capture_context( - context + 1 if isinstance(context, int) else context - ) - ( X, y, @@ -512,7 +502,7 @@ def fit( sample_weight, offset, force_all_finite=self.force_all_finite, - context=captured_context, + context=capture_context(context), ) ######### @@ -547,7 +537,7 @@ def fit( A_ineq = copy.copy(self.A_ineq) b_ineq = copy.copy(self.b_ineq) - cv = check_cv(self.cv) + cv = skl.model_selection.check_cv(self.cv) if self._solver == "cd": _stype = ["csc"] @@ -612,7 +602,7 @@ def _get_deviance(coef): A_ineq, P1_no_alpha, P2_no_alpha, - ) = _standardize( + ) = standardize( x_train, w_train, self._center_predictors, @@ -660,7 +650,7 @@ def _get_deviance(coef): ) if self.fit_intercept: - intercept_path_, coef_path_ = _unstandardize( + intercept_path_, coef_path_ = unstandardize( self.col_means_, self.col_stds_, coef[:, 0], coef[:, 1:] ) assert isinstance(intercept_path_, np.ndarray) # make mypy happy @@ -672,7 +662,7 @@ def _get_deviance(coef): ] else: # set intercept to zero as the other linear models do - intercept_path_, coef_path_ = _unstandardize( + intercept_path_, coef_path_ = unstandardize( self.col_means_, self.col_stds_, np.zeros(coef.shape[0]), coef ) deviance_path_ = [_get_deviance(_coef) for _coef in coef_path_] @@ -680,7 +670,7 @@ def _get_deviance(coef): return intercept_path_, coef_path_, deviance_path_ jobs = ( - delayed(_fit_path)( + joblib.delayed(_fit_path)( self, train_idx=train_idx, test_idx=test_idx, @@ -698,7 +688,8 @@ def _get_deviance(coef): for train_idx, test_idx in cv.split(X, y) for this_l1_ratio, this_alphas in zip(l1_ratio, alphas) ) - paths_data = Parallel(n_jobs=self.n_jobs, prefer="processes")(jobs) + + paths_data = joblib.Parallel(n_jobs=self.n_jobs, prefer="processes")(jobs) self.intercept_path_ = np.reshape( [elmt[0] for elmt in paths_data], @@ -741,7 +732,7 @@ def _get_deviance(coef): A_ineq, P1, P2, - ) = _standardize( + ) = standardize( X, sample_weight, self._center_predictors, @@ -772,12 +763,12 @@ def _get_deviance(coef): ) if self.fit_intercept: - self.intercept_, self.coef_ = _unstandardize( + self.intercept_, self.coef_ = unstandardize( self.col_means_, self.col_stds_, coef[0], coef[1:] ) else: # set intercept to zero as the other linear models do - self.intercept_, self.coef_ = _unstandardize( + self.intercept_, self.coef_ = unstandardize( self.col_means_, self.col_stds_, 0.0, coef ) diff --git a/src/glum/_glm_regressor.py b/src/glum/_glm_regressor.py new file mode 100644 index 000000000..df47c382a --- /dev/null +++ b/src/glum/_glm_regressor.py @@ -0,0 +1,978 @@ +import warnings +from collections.abc import Iterable, Mapping +from typing import Any, Optional, Union + +import formulaic +import numpy as np +import sklearn as skl +import tabmat as tm +from scipy import sparse + +from ._distribution import ExponentialDispersionModel +from ._formula import capture_context +from ._glm import GeneralizedLinearRegressorBase, setup_p1, setup_p2 +from ._linalg import is_pos_semidef +from ._link import Link +from ._typing import ArrayLike, ShapedArrayLike +from ._utils import standardize, unstandardize +from ._validation import check_bounds, check_inequality_constraints + + +class GeneralizedLinearRegressor(GeneralizedLinearRegressorBase): + """Regression via a Generalized Linear Model (GLM) with penalties. + + GLMs based on a reproductive Exponential Dispersion Model (EDM) aimed at + fitting and predicting the mean of the target ``y`` as ``mu=h(X*w)``. + Therefore, the fit minimizes the following objective function with combined + L1 and L2 priors as regularizer:: + + 1/(2*sum(s)) * deviance(y, h(X*w); s) + + alpha * l1_ratio * ||P1*w||_1 + + 1/2 * alpha * (1 - l1_ratio) * w*P2*w + + with inverse link function ``h`` and ``s=sample_weight``. + Note that, for ``alpha=0`` the unregularized GLM is recovered. + This is not the default behavior (see ``alpha`` parameter description for details). + Additionally, for ``sample_weight=None``, one has ``s_i=1`` and + ``sum(s)=n_samples``. For ``P1=P2='identity'``, the penalty is the elastic net:: + + alpha * l1_ratio * ||w||_1 + 1/2 * alpha * (1 - l1_ratio) * ||w||_2^2. + + If you are interested in controlling the L1 and L2 penalties separately, + keep in mind that this is equivalent to:: + + a * L1 + b * L2, + + where:: + + alpha = a + b and l1_ratio = a / (a + b). + + The parameter ``l1_ratio`` corresponds to alpha in the R package glmnet, + while ``alpha`` corresponds to the lambda parameter in glmnet. + Specifically, ``l1_ratio = 1`` is the lasso penalty. + + Read more in :doc:`background`. + + Parameters + ---------- + alpha : {float, array-like}, optional (default=None) + Constant that multiplies the penalty terms and thus determines the + regularization strength. If ``alpha_search`` is ``False`` (the default), + then ``alpha`` must be a scalar or None (equivalent to ``alpha=0``). + If ``alpha_search`` is ``True``, then ``alpha`` must be an iterable or + ``None``. See ``alpha_search`` to find how the regularization path is + set if ``alpha`` is ``None``. See the notes for the exact mathematical + meaning of this parameter. ``alpha=0`` is equivalent to unpenalized + GLMs. In this case, the design matrix ``X`` must have full column rank + (no collinearities). + + l1_ratio : float, optional (default=0) + The elastic net mixing parameter, with ``0 <= l1_ratio <= 1``. For + ``l1_ratio = 0``, the penalty is an L2 penalty. ``For l1_ratio = 1``, it + is an L1 penalty. For ``0 < l1_ratio < 1``, the penalty is a + combination of L1 and L2. + + P1 : {'identity', array-like, None}, shape (n_features,), optional + (default='identity') + This array controls the strength of the regularization for each coefficient + independently. A high value will lead to higher regularization while a value of + zero will remove the regularization on this parameter. + Note that ``n_features = X.shape[1]``. If ``X`` is a pandas DataFrame + with a categorical dtype and P1 has the same size as the number of columns, + the penalty of the categorical column will be applied to all the levels of + the categorical. + + P2 : {'identity', array-like, sparse matrix, None}, shape (n_features,) \ + or (n_features, n_features), optional (default='identity') + With this option, you can set the P2 matrix in the L2 penalty + ``w*P2*w``. This gives a fine control over this penalty (Tikhonov + regularization). A 2d array is directly used as the square matrix P2. A + 1d array is interpreted as diagonal (square) matrix. The default + ``'identity'`` and ``None`` set the identity matrix, which gives the usual + squared L2-norm. If you just want to exclude certain coefficients, pass a 1d + array filled with 1 and 0 for the coefficients to be excluded. Note that P2 must + be positive semi-definite. If ``X`` is a pandas DataFrame with a categorical + dtype and P2 has the same size as the number of columns, the penalty of the + categorical column will be applied to all the levels of the categorical. Note + that if P2 is two-dimensional, its size needs to be of the same length as the + expanded ``X`` matrix. + + fit_intercept : bool, optional (default=True) + Specifies if a constant (a.k.a. bias or intercept) should be + added to the linear predictor (``X * coef + intercept``). + + family : str or ExponentialDispersionModel, optional (default='normal') + The distributional assumption of the GLM, i.e. the loss function to + minimize. If a string, one of: ``'binomial'``, ``'gamma'``, + ``'gaussian'``, ``'inverse.gaussian'``, ``'normal'``, ``'poisson'``, + ``'tweedie'`` or ``'negative.binomial'``. Note that ``'tweedie'`` sets + the power of the Tweedie distribution to 1.5; to use another value, + specify it in parentheses (e.g., ``'tweedie (1.5)'``). The same applies + for ``'negative.binomial'`` and theta parameter. + + link : {'auto', 'identity', 'log', 'logit', 'cloglog'} oe Link, \ + optional (default='auto') + The link function of the GLM, i.e. mapping from linear + predictor (``X * coef``) to expectation (``mu``). Option ``'auto'`` sets + the link depending on the chosen family as follows: + + - ``'identity'`` for family ``'normal'`` + - ``'log'`` for families ``'poisson'``, ``'gamma'``, + ``'inverse.gaussian'`` and ``'negative.binomial'``. + - ``'logit'`` for family ``'binomial'`` + + solver : {'auto', 'irls-cd', 'irls-ls', 'lbfgs', 'trust-constr'}, \ + optional (default='auto') + Algorithm to use in the optimization problem: + + - ``'auto'``: ``'irls-ls'`` if ``l1_ratio`` is zero and ``'irls-cd'`` otherwise. + - ``'irls-cd'``: Iteratively reweighted least squares with a coordinate + descent inner solver. This can deal with L1 as well as L2 penalties. + Note that in order to avoid unnecessary memory duplication of X in the + ``fit`` method, ``X`` should be directly passed as a + Fortran-contiguous Numpy array or sparse CSC matrix. + - ``'irls-ls'``: Iteratively reweighted least squares with a least + squares inner solver. This algorithm cannot deal with L1 penalties. + - ``'lbfgs'``: Scipy's L-BFGS-B optimizer. It cannot deal with L1 + penalties. + - ``'trust-constr'``: Calls + ``scipy.optimize.minimize(method='trust-constr')``. It cannot deal + with L1 penalties. This solver can optimize problems with inequality + constraints, passed via ``A_ineq`` and ``b_ineq``. It will be selected + automatically when inequality constraints are set and + ``solver='auto'``. Note that using this method can lead to + significantly increased runtimes by a factor of ten or higher. + + max_iter : int, optional (default=100) + The maximal number of iterations for solver algorithms. + + max_inner_iter: int, optional (default=100000) + The maximal number of iterations for the inner solver in the IRLS-CD + algorithm. This parameter is only used when ``solver='irls-cd'``. + + gradient_tol : float, optional (default=None) + Stopping criterion. If ``None``, solver-specific defaults will be used. + The default value for most solvers is ``1e-4``, except for + ``'trust-constr'``, which requires more conservative convergence + settings and has a default value of ``1e-8``. + + For the IRLS-LS, L-BFGS and trust-constr solvers, the iteration + will stop when ``max{|g_i|, i = 1, ..., n} <= tol``, where ``g_i`` is + the ``i``-th component of the gradient (derivative) of the objective + function. For the CD solver, convergence is reached when + ``sum_i(|minimum norm of g_i|)``, where ``g_i`` is the subgradient of + the objective and the minimum norm of ``g_i`` is the element of the + subgradient with the smallest L2 norm. + + If you wish to only use a step-size tolerance, set ``gradient_tol`` + to a very small number. + + step_size_tol: float, optional (default=None) + Alternative stopping criterion. For the IRLS-LS and IRLS-CD solvers, the + iteration will stop when the L2 norm of the step size is less than + ``step_size_tol``. This stopping criterion is disabled when + ``step_size_tol`` is ``None``. + + hessian_approx: float, optional (default=0.0) + The threshold below which data matrix rows will be ignored for updating + the Hessian. See the algorithm documentation for the IRLS algorithm + for further details. + + warm_start : bool, optional (default=False) + Whether to reuse the solution of the previous call to ``fit`` + as initialization for ``coef_`` and ``intercept_`` (supersedes + ``start_params``). If ``False`` or if the attribute ``coef_`` does not + exist (first call to ``fit``), ``start_params`` sets the start values + for ``coef_`` and ``intercept_``. + + alpha_search : bool, optional (default=False) + Whether to search along the regularization path for the best alpha. + When set to ``True``, ``alpha`` should either be ``None`` or an + iterable. To determine the regularization path, the following sequence + is used: + + 1. If ``alpha`` is an iterable, use it directly. All other parameters + governing the regularization path are ignored. + 2. If ``min_alpha`` is set, create a path from ``min_alpha`` to the + lowest alpha such that all coefficients are zero. + 3. If ``min_alpha_ratio`` is set, create a path where the ratio of + ``min_alpha / max_alpha = min_alpha_ratio``. + 4. If none of the above parameters are set, use a ``min_alpha_ratio`` of + ``1e-6``. + + alphas : DEPRECATED. Use ``alpha`` instead. + + n_alphas : int, optional (default=100) + Number of alphas along the regularization path + + min_alpha_ratio : float, optional (default=None) + Length of the path. ``min_alpha_ratio=1e-6`` means that + ``min_alpha / max_alpha = 1e-6``. If ``None``, ``1e-6`` is used. + + min_alpha : float, optional (default=None) + Minimum alpha to estimate the model with. The grid will then be created + over ``[max_alpha, min_alpha]``. + + start_params : array-like, shape (n_features*,), optional (default=None) + Relevant only if ``warm_start`` is ``False`` or if ``fit`` is called + for the first time (so that ``self.coef_`` does not exist yet). If + ``None``, all coefficients are set to zero and the start value for the + intercept is the weighted average of ``y`` (If ``fit_intercept`` is + ``True``). If an array, used directly as start values; if + ``fit_intercept`` is ``True``, its first element is assumed to be the + start value for the ``intercept_``. Note that + ``n_features* = X.shape[1] + fit_intercept``, i.e. it includes the + intercept. + + selection : str, optional (default='cyclic') + For the CD solver 'cd', the coordinates (features) can be updated in + either cyclic or random order. If set to ``'random'``, a random + coefficient is updated every iteration rather than looping over features + sequentially in the same order, which often leads to significantly + faster convergence, especially when ``gradient_tol`` is higher than + ``1e-4``. + + random_state : int or RandomState, optional (default=None) + The seed of the pseudo random number generator that selects a random + feature to be updated for the CD solver. If an integer, ``random_state`` + is the seed used by the random number generator; if a + :class:`RandomState` instance, ``random_state`` is the random number + generator; if ``None``, the random number generator is the + :class:`RandomState` instance used by ``np.random``. Used when + ``selection`` is ``'random'``. + + copy_X : bool, optional (default=None) + Whether to copy ``X``. Since ``X`` is never modified by + :class:`GeneralizedLinearRegressor`, this is unlikely to be needed; this + option exists mainly for compatibility with other scikit-learn + estimators. If ``False``, ``X`` will not be copied and there will be an + error if you pass an ``X`` in the wrong format, such as providing + integer ``X`` and float ``y``. If ``None``, ``X`` will not be copied + unless it is in the wrong format. + + check_input : bool, optional (default=True) + Whether to bypass several checks on input: ``y`` values in range of + ``family``, ``sample_weight`` non-negative, ``P2`` positive + semi-definite. Don't use this parameter unless you know what you are + doing. + + verbose : int, optional (default=0) + For the IRLS solver, any positive number will result in a pretty + progress bar showing convergence. This features requires having the + tqdm package installed. For the L-BFGS and ``'trust-constr'`` solvers, + set ``verbose`` to any positive number for verbosity. + + scale_predictors: bool, optional (default=False) + If ``True``, scale all predictors to have standard deviation one. + Should be set to ``True`` if ``alpha > 0`` and if you want coefficients + to be penalized equally. + + Reported coefficient estimates are always at the original scale. + + Advanced developer note: Internally, predictors are always rescaled for + computational reasons, but this only affects results if + ``scale_predictors`` is ``True``. + + lower_bounds : array-like, shape (n_features,), optional (default=None) + Set a lower bound for the coefficients. Setting bounds forces the use + of the coordinate descent solver (``'irls-cd'``). + + upper_bounds : array-like, shape=(n_features,), optional (default=None) + See ``lower_bounds``. + + A_ineq : array-like, shape=(n_constraints, n_features), optional (default=None) + Constraint matrix for linear inequality constraints of the form + ``A_ineq w <= b_ineq``. Setting inequality constraints forces the use + of the local gradient-based solver ``'trust-constr'``, which may + increase runtime significantly. Note that the constraints only apply + to coefficients related to features in ``X``. If you want to constrain + the intercept, add it to the feature matrix ``X`` manually and set + ``fit_intercept==False``. + + b_ineq : array-like, shape=(n_constraints,), optional (default=None) + Constraint vector for linear inequality constraints of the form + ``A_ineq w <= b_ineq``. Refer to the documentation of ``A_ineq`` for + details. + + drop_first : bool, optional (default = False) + If ``True``, drop the first column when encoding categorical variables. + Set this to True when ``alpha=0`` and ``solver='auto'`` to prevent an error + due to a singular feature matrix. In the case of using a formula with + interactions, setting this argument to ``True`` ensures structural + full-rankness (it is equivalent to ``ensure_full_rank`` in formulaic and + tabmat). + + robust : bool, optional (default = False) + If true, then robust standard errors are computed by default. + + expected_information : bool, optional (default = False) + If true, then the expected information matrix is computed by default. + Only relevant when computing robust standard errors. + + formula : formulaic.FormulaSpec + A formula accepted by formulaic. It can either be a one-sided formula, in + which case ``y`` must be specified in ``fit``, or a two-sided formula, in + which case ``y`` must be ``None``. + + interaction_separator: str, default=":" + The separator between the names of interacted variables. + + categorical_format : str, optional, default='{name}[{category}]' + Format string for categorical features. The format string should + contain the placeholder ``{name}`` for the feature name and + ``{category}`` for the category name. Only used if ``X`` is a pandas + DataFrame. + + cat_missing_method: str {'fail'|'zero'|'convert'}, default='fail' + How to handle missing values in categorical columns. Only used if ``X`` + is a pandas data frame. + - if 'fail', raise an error if there are missing values + - if 'zero', missing values will represent all-zero indicator columns. + - if 'convert', missing values will be converted to the ``cat_missing_name`` + category. + + cat_missing_name: str, default='(MISSING)' + Name of the category to which missing values will be converted if + ``cat_missing_method='convert'``. Only used if ``X`` is a pandas data frame. + + Attributes + ---------- + coef_ : numpy.array, shape (n_features,) + Estimated coefficients for the linear predictor (X*coef_+intercept_) in + the GLM. + + intercept_ : float + Intercept (a.k.a. bias) added to linear predictor. + + n_iter_ : int + Actual number of iterations used in solver. + + col_means_: array, shape (n_features,) + The means of the columns of the design matrix ``X``. + + col_stds_: array, shape (n_features,) + The standard deviations of the columns of the design matrix ``X``. + + Notes + ----- + The fit itself does not need outcomes to be from an EDM, but only assumes + the first two moments to be + :math:`\\mu_i \\equiv \\mathrm{E}(y_i) = h(x_i' w)` and + :math:`\\mathrm{var}(y_i) = (\\phi / s_i) v(\\mu_i)`. The unit + variance function :math:`v(\\mu_i)` is a property of and given by the + specific EDM; see :doc:`background`. + + The parameters :math:`w` (``coef_`` and ``intercept_``) are estimated by + minimizing the deviance plus penalty term, which is equivalent to + (penalized) maximum likelihood estimation. + + If the target ``y`` is a ratio, appropriate sample weights ``s`` should be + provided. As an example, consider Poisson distributed counts ``z`` + (integers) and weights ``s = exposure`` (time, money, persons years, ...). + Then you fit ``y ≡ z/s``, i.e. + ``GeneralizedLinearModel(family='poisson').fit(X, y, sample_weight=s)``. The + weights are necessary for the right (finite sample) mean. Consider + :math:`\\bar{y} = \\sum_i s_i y_i / \\sum_i s_i`: in this case, one might + say that :math:`y` follows a 'scaled' Poisson distribution. The same holds + for other distributions. + + References + ---------- + For the coordinate descent implementation: + * Guo-Xun Yuan, Chia-Hua Ho, Chih-Jen Lin + An Improved GLMNET for L1-regularized Logistic Regression, + Journal of Machine Learning Research 13 (2012) 1999-2030 + https://www.csie.ntu.edu.tw/~cjlin/papers/l1_glmnet/long-glmnet.pdf + """ + + def __init__( + self, + *, + alpha=None, + l1_ratio=0, + P1: Optional[Union[str, np.ndarray]] = "identity", + P2: Optional[Union[str, np.ndarray, sparse.spmatrix]] = "identity", + fit_intercept=True, + family: Union[str, ExponentialDispersionModel] = "normal", + link: Union[str, Link] = "auto", + solver: str = "auto", + max_iter=100, + max_inner_iter=100000, + gradient_tol: Optional[float] = None, + step_size_tol: Optional[float] = None, + hessian_approx: float = 0.0, + warm_start: bool = False, + alpha_search: bool = False, + alphas: Optional[np.ndarray] = None, + n_alphas: int = 100, + min_alpha_ratio: Optional[float] = None, + min_alpha: Optional[float] = None, + start_params: Optional[np.ndarray] = None, + selection: str = "cyclic", + random_state=None, + copy_X: Optional[bool] = None, + check_input: bool = True, + verbose=0, + scale_predictors: bool = False, + lower_bounds: Optional[np.ndarray] = None, + upper_bounds: Optional[np.ndarray] = None, + A_ineq: Optional[np.ndarray] = None, + b_ineq: Optional[np.ndarray] = None, + force_all_finite: bool = True, + drop_first: bool = False, + robust: bool = True, + expected_information: bool = False, + formula: Optional[formulaic.FormulaSpec] = None, + interaction_separator: str = ":", + categorical_format: str = "{name}[{category}]", + cat_missing_method: str = "fail", + cat_missing_name: str = "(MISSING)", + ): + self.alphas = alphas + self.alpha = alpha + super().__init__( + l1_ratio=l1_ratio, + P1=P1, + P2=P2, + fit_intercept=fit_intercept, + family=family, + link=link, + solver=solver, + max_iter=max_iter, + max_inner_iter=max_inner_iter, + gradient_tol=gradient_tol, + step_size_tol=step_size_tol, + hessian_approx=hessian_approx, + warm_start=warm_start, + alpha_search=alpha_search, + n_alphas=n_alphas, + min_alpha=min_alpha, + min_alpha_ratio=min_alpha_ratio, + start_params=start_params, + selection=selection, + random_state=random_state, + copy_X=copy_X, + check_input=check_input, + verbose=verbose, + scale_predictors=scale_predictors, + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + A_ineq=A_ineq, + b_ineq=b_ineq, + force_all_finite=force_all_finite, + drop_first=drop_first, + robust=robust, + expected_information=expected_information, + formula=formula, + interaction_separator=interaction_separator, + categorical_format=categorical_format, + cat_missing_method=cat_missing_method, + cat_missing_name=cat_missing_name, + ) + + def _validate_hyperparameters(self) -> None: + if self.alpha_search: + if not isinstance(self.alpha, Iterable) and self.alpha is not None: + raise ValueError( + "`alpha` should be an Iterable or None when `alpha_search`" + " is True" + ) + if self.alpha is not None and ( + (np.asarray(self.alpha) < 0).any() + or not np.issubdtype(np.asarray(self.alpha).dtype, np.number) + ): + raise ValueError("`alpha` must contain only non-negative numbers") + if not self.alpha_search: + if not np.isscalar(self.alpha) and self.alpha is not None: + raise ValueError( + "`alpha` should be a scalar or None when `alpha_search`" " is False" + ) + if self.alpha is not None and ( + not isinstance(self.alpha, (int, float)) or self.alpha < 0 + ): + raise ValueError( + "Penalty term must be a non-negative number;" + f" got (alpha={self.alpha})" # type: ignore + ) + + if ( + not np.isscalar(self.l1_ratio) + # check for numeric, i.e. not a string + or not np.issubdtype(np.asarray(self.l1_ratio).dtype, np.number) + or self.l1_ratio < 0 # type: ignore + or self.l1_ratio > 1 # type: ignore + ): + raise ValueError( + "l1_ratio must be a number in interval [0, 1];" + f" got (l1_ratio={self.l1_ratio})" + ) + super()._validate_hyperparameters() + + def fit( + self, + X: ArrayLike, + y: Optional[ArrayLike] = None, + sample_weight: Optional[ArrayLike] = None, + offset: Optional[ArrayLike] = None, + *, + store_covariance_matrix: bool = False, + clusters: Optional[np.ndarray] = None, + # TODO: take out weights_sum (or use it properly) + weights_sum: Optional[float] = None, + context: Optional[Union[int, Mapping[str, Any]]] = None, + ): + """Fit a Generalized Linear Model. + + Parameters + ---------- + X : {array-like, sparse matrix}, shape (n_samples, n_features) + Training data. Note that a ``float32`` matrix is acceptable and will + result in the entire algorithm being run in 32-bit precision. + However, for problems that are poorly conditioned, this might result + in poor convergence or flawed parameter estimates. If a Pandas data + frame is provided, it may contain categorical columns. In that case, + a separate coefficient will be estimated for each category. No + category is omitted. This means that some regularization is required + to fit models with an intercept or models with several categorical + columns. + + y : array-like, shape (n_samples,) + Target values. + + sample_weight : array-like, shape (n_samples,), optional (default=None) + Individual weights w_i for each sample. Note that, for an + Exponential Dispersion Model (EDM), one has + :math:`\\mathrm{var}(y_i) = \\phi \\times v(mu) / w_i`. + If :math:`y_i \\sim EDM(\\mu, \\phi / w_i)`, then + :math:`\\sum w_i y_i / \\sum w_i \\sim EDM(\\mu, \\phi / \\sum w_i)`, + i.e. the mean of :math:`y` is a weighted average with weights equal + to ``sample_weight``. + + offset: array-like, shape (n_samples,), optional (default=None) + Added to linear predictor. An offset of 3 will increase expected + ``y`` by 3 if the link is linear and will multiply expected ``y`` by + 3 if the link is logarithmic. + + store_covariance_matrix : bool, optional (default=False) + Whether to estimate and store the covariance matrix of the parameter + estimates. If ``True``, the covariance matrix will be available in the + ``covariance_matrix_`` attribute after fitting. + + clusters : array-like, optional, default=None + Array with cluster membership. Clustered standard errors are + computed if clusters is not None. + + context : Optional[Union[int, Mapping[str, Any]]], default=None + The context to add to the evaluation context of the formula with, + e.g., custom transforms. If an integer, the context is taken from + the stack frame of the caller at the given depth. Otherwise, a + mapping from variable names to values is expected. By default, + no context is added. Set ``context=0`` to make the calling scope + available. + + weights_sum: float, optional (default=None) + + Returns + ------- + self + """ + + self._validate_hyperparameters() + + # NOTE: This function checks if all the entries in X and y are + # finite. That can be expensive. But probably worthwhile. + ( + X, + y, + sample_weight, + offset, + weights_sum, + P1, + P2, + ) = self._set_up_and_check_fit_args( + X, + y, + sample_weight, + offset, + force_all_finite=self.force_all_finite, + context=capture_context(context), + ) + + assert isinstance(X, tm.MatrixBase) + assert isinstance(y, np.ndarray) + + self._set_up_for_fit(y) + + # 1.3 arguments to take special care ################################## + # P1, P2, start_params + stype = ["csc"] if self._solver == "irls-cd" else ["csc", "csr"] + P1_no_alpha = setup_p1(P1, X, X.dtype, 1, self.l1_ratio) + P2_no_alpha = setup_p2(P2, X, stype, X.dtype, 1, self.l1_ratio) + + lower_bounds = check_bounds(self.lower_bounds, X.shape[1], X.dtype) + upper_bounds = check_bounds(self.upper_bounds, X.shape[1], X.dtype) + + A_ineq, b_ineq = check_inequality_constraints( + self.A_ineq, self.b_ineq, n_features=X.shape[1], dtype=X.dtype + ) + + if (lower_bounds is not None) and (upper_bounds is not None): + if np.any(lower_bounds > upper_bounds): + raise ValueError("Upper bounds must be higher than lower bounds.") + + # 1.4 additional validations ########################################## + if self.check_input: + # check if P2 is positive semidefinite + if not isinstance(self.P2, str): # self.P2 != 'identity' + if not is_pos_semidef(P2_no_alpha): + if P2_no_alpha.ndim == 1 or P2_no_alpha.shape[0] == 1: + error = "1d array P2 must not have negative values." + else: + error = "P2 must be positive semi-definite." + raise ValueError(error) + # TODO: if alpha=0 check that X is not rank deficient + # TODO: what else to check? + + ####################################################################### + # 2c. potentially rescale predictors + ####################################################################### + + ( + X, + self.col_means_, + self.col_stds_, + lower_bounds, + upper_bounds, + A_ineq, + P1_no_alpha, + P2_no_alpha, + ) = standardize( + X, + sample_weight, + self._center_predictors, + self.scale_predictors, + lower_bounds, + upper_bounds, + A_ineq, + P1_no_alpha, + P2_no_alpha, + ) + + ####################################################################### + # 3. initialization of coef = (intercept_, coef_) # + ####################################################################### + + coef = self._get_start_coef( + X, + y, + sample_weight, + offset, + self.col_means_, + self.col_stds_, + dtype=[np.float64, np.float32], + ) + + ####################################################################### + # 4. fit # + ####################################################################### + if self.alpha_search: + if self.alphas is not None: + warnings.warn( + "alphas is deprecated. Use alpha instead.", DeprecationWarning + ) + self._alphas = self.alphas + elif self.alpha is None: + self._alphas = self._get_alpha_path( + P1_no_alpha=P1_no_alpha, X=X, y=y, w=sample_weight, offset=offset + ) + else: + self._alphas = self.alpha + if self.min_alpha is not None or self.min_alpha_ratio is not None: + warnings.warn( + "`alpha` is set. Ignoring `min_alpha` and `min_alpha_ratio`." + ) + + coef = self._solve_regularization_path( + X=X, + y=y, + sample_weight=sample_weight, + P2_no_alpha=P2_no_alpha, + P1_no_alpha=P1_no_alpha, + alphas=self._alphas, + coef=coef, + offset=offset, + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + A_ineq=A_ineq, + b_ineq=b_ineq, + ) + + # intercept_ and coef_ return the last estimated alpha + if self.fit_intercept: + self.intercept_path_, self.coef_path_ = unstandardize( + self.col_means_, self.col_stds_, coef[:, 0], coef[:, 1:] + ) + self.intercept_ = self.intercept_path_[-1] # type: ignore + self.coef_ = self.coef_path_[-1] + else: + # set intercept to zero as the other linear models do + self.intercept_path_, self.coef_path_ = unstandardize( + self.col_means_, self.col_stds_, np.zeros(coef.shape[0]), coef + ) + self.intercept_ = 0.0 + self.coef_ = self.coef_path_[-1] + else: + if self.alpha is None: + _alpha = 0.0 + else: + _alpha = self.alpha + if _alpha > 0 and self.l1_ratio > 0 and self._solver != "irls-cd": + raise ValueError( + f"The chosen solver (solver={self._solver}) can't deal " + "with L1 penalties, which are included with " + f"(alpha={_alpha}) and (l1_ratio={self.l1_ratio})." + ) + coef = self._solve( + X=X, + y=y, + sample_weight=sample_weight, + P2=P2_no_alpha * _alpha, + P1=P1_no_alpha * _alpha, + coef=coef, + offset=offset, + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + A_ineq=A_ineq, + b_ineq=b_ineq, + ) + + if self.fit_intercept: + self.intercept_, self.coef_ = unstandardize( + self.col_means_, self.col_stds_, coef[0], coef[1:] + ) + else: + # set intercept to zero as the other linear models do + self.intercept_, self.coef_ = unstandardize( + self.col_means_, self.col_stds_, 0.0, coef + ) + + self.covariance_matrix_ = None + if store_covariance_matrix: + self.covariance_matrix( + X=X.unstandardize(), + y=y, + offset=offset, + sample_weight=sample_weight * weights_sum, + robust=getattr(self, "robust", True), + clusters=clusters, + expected_information=getattr(self, "expected_information", False), + store_covariance_matrix=True, + skip_checks=True, + ) + + return self + + def _compute_information_criteria( + self, + X: ShapedArrayLike, + y: ShapedArrayLike, + sample_weight: Optional[ArrayLike] = None, + context: Optional[Mapping[str, Any]] = None, + ): + """ + Computes and stores the model's degrees of freedom, the 'aic', 'aicc' + and 'bic' information criteria. + + The model's degrees of freedom are used to calculate the effective + number of parameters. This uses the claim by [2] and [3] that, for + L1 regularisation, the number of non-zero parameters in the trained model + is an unbiased approximator of the degrees of freedom of the model. Note + that this might not hold true for L2 regularisation and thus we raise a + warning for this case. + + References + ---------- + [1] Burnham KP, Anderson KR (2002). Model Selection and Multimodel + Inference; Springer New York. + [2] Zou, H., Hastie, T. and Tibshirani, R., (2007). On the “degrees of + freedom” of the lasso; The Annals of Statistics. + [3] Park, M.Y., 2006. Generalized linear models with regularization; + Stanford Universty. + """ + if not hasattr(self.family_instance, "log_likelihood"): + raise NotImplementedError( + "The family instance does not define a `log_likelihood` method, so " + "information criteria cannot be computed. Compatible families include " + "the binomial, negative binomial and Tweedie (power<=2 or power=3)." + ) + + ddof = np.sum(np.abs(self.coef_) > np.finfo(self.coef_.dtype).eps) # type: ignore + k_params = ddof + self.fit_intercept + nobs = X.shape[0] + + if nobs != self._num_obs: + raise ValueError( + "The same dataset that was used for training should also be used for " + "the computation of information criteria." + ) + + mu = self.predict(X, context=context) + ll = self.family_instance.log_likelihood(y, mu, sample_weight=sample_weight) + + aic = -2 * ll + 2 * k_params + bic = -2 * ll + np.log(nobs) * k_params + + if nobs > k_params + 1: + aicc = aic + 2 * k_params * (k_params + 1) / (nobs - k_params - 1) + else: + aicc = None + + self._info_criteria = {"aic": aic, "aicc": aicc, "bic": bic} + + return True + + def aic( + self, + X: ArrayLike, + y: ArrayLike, + sample_weight: Optional[ArrayLike] = None, + *, + context: Optional[Union[int, Mapping[str, Any]]] = None, + ): + """ + Akaike's information criteria. Computed as: + :math:`-2\\log\\hat{\\mathcal{L}} + 2\\hat{k}` where + :math:`\\hat{\\mathcal{L}}` is the maximum likelihood estimate of the + model, and :math:`\\hat{k}` is the effective number of parameters. See + `_compute_information_criteria` for more information on the computation + of :math:`\\hat{k}`. + + Parameters + ---------- + X : {array-like, sparse matrix}, shape (n_samples, n_features) + Same data as used in 'fit' + + y : array-like, shape (n_samples,) + Same data as used in 'fit' + + sample_weight : array-like, shape (n_samples,), optional (default=None) + Same data as used in 'fit' + + context : Optional[Union[int, Mapping[str, Any]]], default=None + The context to add to the evaluation context of the formula with, + e.g., custom transforms. If an integer, the context is taken from + the stack frame of the caller at the given depth. Otherwise, a + mapping from variable names to values is expected. By default, + no context is added. Set ``context=0`` to make the calling scope + available. + """ + return self._get_info_criteria("aic", X, y, sample_weight, context=context) + + def aicc( + self, + X: ArrayLike, + y: ArrayLike, + sample_weight: Optional[ArrayLike] = None, + *, + context: Optional[Union[int, Mapping[str, Any]]] = None, + ): + """ + Second-order Akaike's information criteria (or small sample AIC). + Computed as: + :math:`-2\\log\\hat{\\mathcal{L}} + 2\\hat{k} + \\frac{2k(k+1)}{n-k-1}` + where :math:`\\hat{\\mathcal{L}}` is the maximum likelihood estimate of + the model, :math:`n` is the number of training instances, and + :math:`\\hat{k}` is the effective number of parameters. See + `_compute_information_criteria` for more information on the computation + of :math:`\\hat{k}`. + + Parameters + ---------- + X : {array-like, sparse matrix}, shape (n_samples, n_features) + Same data as used in 'fit' + + y : array-like, shape (n_samples,) + Same data as used in 'fit' + + sample_weight : array-like, shape (n_samples,), optional (default=None) + Same data as used in 'fit' + + context : Optional[Union[int, Mapping[str, Any]]], default=None + The context to add to the evaluation context of the formula with, + e.g., custom transforms. If an integer, the context is taken from + the stack frame of the caller at the given depth. Otherwise, a + mapping from variable names to values is expected. By default, + no context is added. Set ``context=0`` to make the calling scope + available. + """ + aicc = self._get_info_criteria("aicc", X, y, sample_weight, context=context) + + if not aicc: + msg = "Model degrees of freedom should be more than training data points." + raise ValueError(msg) + + return aicc + + def bic( + self, + X: ArrayLike, + y: ArrayLike, + sample_weight: Optional[ArrayLike] = None, + *, + context: Optional[Union[int, Mapping[str, Any]]] = None, + ): + """ + Bayesian information criterion. Computed as: + :math:`-2\\log\\hat{\\mathcal{L}} + k\\log(n)` where + :math:`\\hat{\\mathcal{L}}` is the maximum likelihood estimate of the + model, :math:`n` is the number of training instances, and + :math:`\\hat{k}` is the effective number of parameters. See + `_compute_information_criteria` for more information on the computation + of :math:`\\hat{k}`. + + Parameters + ---------- + X : {array-like, sparse matrix}, shape (n_samples, n_features) + Same data as used in 'fit' + + y : array-like, shape (n_samples,) + Same data as used in 'fit' + + sample_weight : array-like, shape (n_samples,), optional (default=None) + Same data as used in 'fit' + + context : Optional[Union[int, Mapping[str, Any]]], default=None + The context to add to the evaluation context of the formula with, + e.g., custom transforms. If an integer, the context is taken from + the stack frame of the caller at the given depth. Otherwise, a + mapping from variable names to values is expected. By default, + no context is added. Set ``context=0`` to make the calling scope + available. + """ + return self._get_info_criteria("bic", X, y, sample_weight, context=context) + + def _get_info_criteria( + self, + crit: str, + X: ArrayLike, + y: ArrayLike, + sample_weight: Optional[ArrayLike] = None, + context: Optional[Union[int, Mapping[str, Any]]] = None, + ): + skl.utils.validation.check_is_fitted(self, "coef_") + + context = capture_context(context) + + if not hasattr(self, "_info_criteria"): + self._compute_information_criteria(X, y, sample_weight, context=context) + + if ( + self.alpha is None or (self.alpha is not None and self.alpha > 0) + ) and self.l1_ratio < 1.0: + warnings.warn( + "There is no general definition for the model's degrees of " + + f"freedom under L2 (ridge) regularisation. The {crit} " + + "might not be well defined in these cases." + ) + + return self._info_criteria[crit] diff --git a/src/glum/_linalg.py b/src/glum/_linalg.py new file mode 100644 index 000000000..f54d64733 --- /dev/null +++ b/src/glum/_linalg.py @@ -0,0 +1,93 @@ +import typing +from typing import Union + +import numpy as np +import tabmat as tm +from scipy import linalg, sparse + + +def is_pos_semidef(p: Union[sparse.spmatrix, np.ndarray]) -> Union[bool, np.bool_]: + """ + Checks for positive semidefiniteness of ``p`` if ``p`` is a matrix, or + ``diag(p)`` if a vector. + + ``np.linalg.cholesky(P2)`` 'only' asserts positive definiteness; due to + numerical precision, we allow eigenvalues to be a tiny bit negative. + """ + # 1d case + if p.ndim == 1 or p.shape[0] == 1: + any_negative = (p < 0).max() if sparse.isspmatrix(p) else (p < 0).any() + return not any_negative + + # 2d case + # About -6e-7 for 32-bit, -1e-15 for 64-bit + epsneg = -10 * np.finfo(np.result_type(float, p.dtype)).epsneg + + if sparse.issparse(p): + # Computing eigenvalues for sparse matrices is inefficient. If the matrix is + # not huge, convert to dense. Otherwise, calculate 10% of its eigenvalues. + p = typing.cast(sparse.spmatrix, p) + if p.shape[0] < 2000: + eigenvalues = linalg.eigvalsh(p.toarray()) + else: + n_evals_to_compuate = p.shape[0] // 10 + 1 + sigma = -1000 * epsneg # start searching near this value + which = "SA" # find smallest algebraic eigenvalues first + eigenvalues = linalg.eigsh( + p, + k=n_evals_to_compuate, + sigma=sigma, + which=which, + return_eigenvectors=False, + ) + else: # dense + eigenvalues = linalg.eigvalsh(p) + + return np.all(eigenvalues >= epsneg) + + +def _safe_lin_pred( + X: Union[tm.MatrixBase, tm.StandardizedMatrix], + coef: np.ndarray, + offset: np.ndarray = None, +) -> np.ndarray: + """Compute the linear predictor taking care if intercept is present.""" + idx_offset = 0 if X.shape[1] == coef.shape[0] else 1 + nonzero_coefs = np.where(coef[idx_offset:] != 0.0)[0].astype(np.int32) + res = X.matvec(coef[idx_offset:], cols=nonzero_coefs) + + if idx_offset == 1: + res += coef[0] + if offset is not None: + return res + offset + return res + + +def _safe_sandwich_dot( + X: Union[tm.MatrixBase, tm.StandardizedMatrix], + d: np.ndarray, + rows: np.ndarray = None, + cols: np.ndarray = None, + intercept=False, +) -> np.ndarray: + """ + Compute sandwich product ``X.T @ diag(d) @ X``. + + With ``intercept=True``, ``X`` is treated as if a column of 1 were appended + as first column of ``X``. ``X`` can be sparse; ``d`` must be an ndarray. + Always returns an ndarray. + """ + result = X.sandwich(d, rows, cols) + if isinstance(result, sparse.dia_matrix): + result = result.toarray() + + if intercept: + dim = result.shape[0] + 1 + res_including_intercept = np.empty((dim, dim), dtype=X.dtype) + res_including_intercept[0, 0] = d.sum() + res_including_intercept[1:, 0] = X.transpose_matvec(d, rows, cols) + res_including_intercept[0, 1:] = res_including_intercept[1:, 0] + res_including_intercept[1:, 1:] = result + else: + res_including_intercept = result + return res_including_intercept diff --git a/src/glum/_solvers.py b/src/glum/_solvers.py index e169bdf41..5e33eea35 100644 --- a/src/glum/_solvers.py +++ b/src/glum/_solvers.py @@ -17,8 +17,8 @@ identify_active_rows, ) from ._distribution import ExponentialDispersionModel, get_one_over_variance +from ._linalg import _safe_lin_pred, _safe_sandwich_dot from ._link import Link -from ._util import _safe_lin_pred, _safe_sandwich_dot def timeit(runtime_attr: str): diff --git a/src/glum/_typing.py b/src/glum/_typing.py new file mode 100644 index 000000000..2d21461bc --- /dev/null +++ b/src/glum/_typing.py @@ -0,0 +1,31 @@ +from typing import NamedTuple, Union + +import numpy as np +import pandas as pd +import scipy.sparse +import tabmat as tm + +VectorLike = Union[np.ndarray, pd.api.extensions.ExtensionArray, pd.Index, pd.Series] + +ArrayLike = Union[ + list, + tm.MatrixBase, + tm.StandardizedMatrix, + pd.DataFrame, + scipy.sparse.spmatrix, + VectorLike, +] + +ShapedArrayLike = Union[ + tm.MatrixBase, + tm.StandardizedMatrix, + pd.DataFrame, + scipy.sparse.spmatrix, + VectorLike, +] + + +class WaldTestResult(NamedTuple): + test_statistic: float + p_value: float + df: int diff --git a/src/glum/_util.py b/src/glum/_utils.py similarity index 53% rename from src/glum/_util.py rename to src/glum/_utils.py index 5f4cbafc7..9bb54aa5f 100644 --- a/src/glum/_util.py +++ b/src/glum/_utils.py @@ -1,23 +1,16 @@ import logging -import warnings from collections.abc import Sequence -from functools import wraps -from typing import Union +from typing import Any, Optional, Union import numpy as np import pandas as pd +import tabmat as tm from scipy import sparse -from tabmat import MatrixBase, StandardizedMatrix _logger = logging.getLogger(__name__) -def _asanyarray(x, **kwargs): - """``np.asanyarray`` with passthrough for scalars.""" - return x if pd.api.types.is_scalar(x) else np.asanyarray(x, **kwargs) - - -def _align_df_categories( +def align_df_categories( df, dtypes, has_missing_category, cat_missing_method ) -> pd.DataFrame: """Align data types for prediction. @@ -29,7 +22,7 @@ def _align_df_categories( Parameters ---------- df : pandas.DataFrame - dtypes : Dict[str, Union[str, type, pandas.core.dtypes.base.ExtensionDtype]] + dtypes : Dict[str, str | type | pandas.core.dtypes.base.ExtensionDtype] has_missing_category : Dict[str, bool] missing_method : str """ @@ -74,7 +67,7 @@ def _align_df_categories( return df -def _add_missing_categories( +def add_missing_categories( df, dtypes, feature_names: Sequence[str], @@ -112,7 +105,7 @@ def _add_missing_categories( return df -def _expand_categorical_penalties( +def expand_categorical_penalties( penalty, X, drop_first, has_missing_category ) -> Union[np.ndarray, str]: """Determine penalty matrices ``P1`` or ``P2`` after expanding categorical columns. @@ -149,64 +142,17 @@ def _expand_categorical_penalties( return penalty -def _is_contiguous(X) -> bool: +def is_contiguous(X) -> bool: if isinstance(X, np.ndarray): return X.flags["C_CONTIGUOUS"] or X.flags["F_CONTIGUOUS"] elif isinstance(X, pd.DataFrame): - return _is_contiguous(X.values) + return is_contiguous(X.values) else: # If not a numpy array or pandas data frame, we assume it is contiguous. return True -def _safe_lin_pred( - X: Union[MatrixBase, StandardizedMatrix], - coef: np.ndarray, - offset: np.ndarray = None, -) -> np.ndarray: - """Compute the linear predictor taking care if intercept is present.""" - idx_offset = 0 if X.shape[1] == coef.shape[0] else 1 - nonzero_coefs = np.where(coef[idx_offset:] != 0.0)[0].astype(np.int32) - res = X.matvec(coef[idx_offset:], cols=nonzero_coefs) - - if idx_offset == 1: - res += coef[0] - if offset is not None: - return res + offset - return res - - -def _safe_sandwich_dot( - X: Union[MatrixBase, StandardizedMatrix], - d: np.ndarray, - rows: np.ndarray = None, - cols: np.ndarray = None, - intercept=False, -) -> np.ndarray: - """ - Compute sandwich product ``X.T @ diag(d) @ X``. - - With ``intercept=True``, ``X`` is treated as if a column of 1 were appended - as first column of ``X``. ``X`` can be sparse; ``d`` must be an ndarray. - Always returns an ndarray. - """ - result = X.sandwich(d, rows, cols) - if isinstance(result, sparse.dia_matrix): - result = result.toarray() - - if intercept: - dim = result.shape[0] + 1 - res_including_intercept = np.empty((dim, dim), dtype=X.dtype) - res_including_intercept[0, 0] = d.sum() - res_including_intercept[1:, 0] = X.transpose_matvec(d, rows, cols) - res_including_intercept[0, 1:] = res_including_intercept[1:, 0] - res_including_intercept[1:, 1:] = result - else: - res_including_intercept = result - return res_including_intercept - - -def _safe_toarray(X) -> np.ndarray: +def safe_toarray(X) -> np.ndarray: """Return a numpy array.""" if sparse.issparse(X): return X.toarray() @@ -214,36 +160,122 @@ def _safe_toarray(X) -> np.ndarray: return np.asarray(X) -def _positional_args_deprecated(unchanged_args=(), unchanged_args_number=None): +def standardize( + X: tm.MatrixBase, + sample_weight: np.ndarray, + center_predictors: bool, + estimate_as_if_scaled_model: bool, + lower_bounds: Optional[np.ndarray], + upper_bounds: Optional[np.ndarray], + A_ineq: Optional[np.ndarray], + P1: Union[np.ndarray, sparse.spmatrix], + P2: Union[np.ndarray, sparse.spmatrix], +) -> tuple[ + tm.StandardizedMatrix, + np.ndarray, + Optional[np.ndarray], + Optional[np.ndarray], + Optional[np.ndarray], + Optional[np.ndarray], + Any, + Any, +]: """ - Raise a FutureWarning if more than `unchanged_args_number` positional - arguments are passed. + Standardize the data matrix ``X`` and adjust the bounds and penalties to + match the standardized data matrix, so that standardizing does not affect + estimates. + + This is only done for computational reasons and does not affect final + estimates or alter the input data. Columns are always scaled to have unit + standard deviation. + + Bounds, inequality constraints and regularization coefficients are modified + appropriately so that the estimates remain unchanged compared to an + unstandardized problem. + + Parameters + ---------- + X : MatrixBase + sample_weight : numpy.ndarray + center_predictors : bool + If ``True``, adjust the data matrix so that columns have mean zero. + estimate_as_if_scaled_model : bool + If ``True``, estimates returned equal those from a model where + predictors have been standardized to have unit standard deviation, with + penalty unchanged. Note that, internally, for purely computational + reasons, we always scale predictors; whether estimates match a scaled + model depends on whether we modify the penalty. If ``False``, penalties + are rescaled to match the original scale, canceling out the effect of + rescaling X. + lower_bounds + upper_bounds + A_ineq + P1 + P2 """ - if unchanged_args_number is None: - unchanged_args_number = len(unchanged_args) - - def decorator(func): - first_part = "Arguments" if unchanged_args else "All arguments" - exceptions = ( - " other than " + ", ".join(f"`{arg}`" for arg in unchanged_args) - if unchanged_args - else "" + X, col_means, col_stds = X.standardize(sample_weight, center_predictors, True) + + if col_stds is not None: + inv_col_stds = _one_over_var_inf_to_val(col_stds, 1.0) + # We copy the bounds when multiplying here so the we avoid + # side effects. + if lower_bounds is not None: + lower_bounds = lower_bounds / inv_col_stds + if upper_bounds is not None: + upper_bounds = upper_bounds / inv_col_stds + if A_ineq is not None: + A_ineq = A_ineq * inv_col_stds + + if not estimate_as_if_scaled_model and col_stds is not None: + P1 *= inv_col_stds + if sparse.issparse(P2): + inv_col_stds_mat = sparse.diags(inv_col_stds) + P2 = inv_col_stds_mat @ P2 @ inv_col_stds_mat + elif P2.ndim == 1: + P2 *= inv_col_stds**2 + else: + P2 = (inv_col_stds[:, None] * P2) * inv_col_stds[None, :] + + return X, col_means, col_stds, lower_bounds, upper_bounds, A_ineq, P1, P2 + + +def standardize_warm_start( # noda D + coef: np.ndarray, col_means: np.ndarray, col_stds: Optional[np.ndarray] +) -> None: + if col_stds is None: + coef[0] += np.squeeze(col_means).dot(coef[1:]) + else: + coef[1:] *= col_stds + coef[0] += np.squeeze(col_means * _one_over_var_inf_to_val(col_stds, 1)).dot( + coef[1:] ) - msg = ( - f"{first_part} to `{func.__qualname__}`{exceptions} " - "will become keyword-only in 3.0.0." + +def unstandardize( # noda D + col_means: np.ndarray, + col_stds: Optional[np.ndarray], + intercept: Union[float, np.ndarray], + coef: np.ndarray, +) -> tuple[Union[float, np.ndarray], np.ndarray]: + if col_stds is None: + intercept -= np.squeeze(np.squeeze(col_means).dot(np.atleast_1d(coef).T)) + else: + penalty_mult = _one_over_var_inf_to_val(col_stds, 1.0) + intercept -= np.squeeze( + np.squeeze(col_means * penalty_mult).dot(np.atleast_1d(coef).T) ) + coef *= penalty_mult + return intercept, coef - @wraps(func) - def wrapper(*args, **kwargs): - if len(args) > unchanged_args_number + 1: # +1 for self - warnings.warn( - msg, - FutureWarning, - ) - return func(*args, **kwargs) - return wrapper +def _one_over_var_inf_to_val(arr: np.ndarray, val: float) -> np.ndarray: + """ + Return 1/arr unless the values are zeros. - return decorator + If values are zeros, return val. + """ + zeros = np.where(np.abs(arr) < 10 * np.sqrt(np.finfo(arr.dtype).eps)) + with np.errstate(divide="ignore"): + one_over = 1 / arr + one_over[zeros] = val + return one_over diff --git a/src/glum/_validation.py b/src/glum/_validation.py new file mode 100644 index 000000000..079ab5516 --- /dev/null +++ b/src/glum/_validation.py @@ -0,0 +1,217 @@ +import copy +import typing + +import numpy as np +import packaging.version +import pandas as pd +import sklearn as skl +import tabmat as tm +from scipy import sparse + +from ._typing import ArrayLike, VectorLike + +if packaging.version.parse(skl.__version__).release < (1, 6): + keyword_finiteness = "force_all_finite" + validate_data = skl.base.BaseEstimator._validate_data +else: + keyword_finiteness = "ensure_all_finite" + + +def check_array_tabmat_compliant(mat: ArrayLike, drop_first: bool = False, **kwargs): + to_copy = kwargs.get("copy", False) + + if isinstance(mat, pd.DataFrame): + raise RuntimeError("DataFrames should have been converted by this point.") + + if isinstance(mat, tm.SplitMatrix): + kwargs.update({"ensure_min_features": 0}) + new_matrices = [ + check_array_tabmat_compliant(m, drop_first=drop_first, **kwargs) + for m in mat.matrices + ] + new_indices = [elt.copy() for elt in mat.indices] if to_copy else mat.indices + return tm.SplitMatrix(new_matrices, new_indices) + + if isinstance(mat, tm.CategoricalMatrix): + if to_copy: + return copy.copy(mat) + return mat + + if isinstance(mat, tm.StandardizedMatrix): + return tm.StandardizedMatrix( + check_array_tabmat_compliant(mat.mat, drop_first=drop_first, **kwargs), + skl.utils.check_array(mat.shift, **kwargs), + ) + + original_type = type(mat) + if isinstance(mat, (tm.DenseMatrix, tm.SparseMatrix)): + res = skl.utils.check_array(mat.unpack(), **kwargs) + else: + res = skl.utils.check_array(mat, **kwargs) + + if res is not mat and original_type in (tm.DenseMatrix, tm.SparseMatrix): + res = original_type( + res, + column_names=mat.column_names, # type: ignore + term_names=mat.term_names, # type: ignore + ) + + return res + + +def check_X_y_tabmat_compliant( + X: ArrayLike, y: typing.Union[VectorLike, sparse.spmatrix], **kwargs +) -> tuple[typing.Union[tm.MatrixBase, sparse.spmatrix, np.ndarray], np.ndarray]: + """ + See the documentation for :func:`sklearn.utils.check_X_y`. This function + behaves identically for inputs that are not from the Matrix package and + fixes some parameters, such as ``'force_all_finite'``, to match the needs of + GLMs. + + Returns + ------- + X_converted : array-like + The converted and validated X. + y_converted : numpy.ndarray + The converted and validated y. + """ + if y is None: + raise ValueError("y cannot be None") + + y = skl.utils.column_or_1d(y, warn=True) + + skl.utils.assert_all_finite(y) + skl.utils.check_consistent_length(X, y) + + if y.dtype.kind == "O": + y = y.astype(np.float64) + + X = check_array_tabmat_compliant(X, **kwargs) + + return X, y + + +def check_bounds( + bounds: typing.Optional[typing.Union[float, VectorLike]], n_features: int, dtype +) -> typing.Optional[np.ndarray]: + """Check that the bounds have the right shape.""" + if bounds is None: + return None + if np.isscalar(bounds): + return np.full(n_features, bounds, dtype=dtype) + + bounds = skl.utils.check_array( + bounds, + accept_sparse=False, + ensure_2d=False, + dtype=dtype, + **{keyword_finiteness: False}, + ) + + bounds = typing.cast(np.ndarray, bounds) + + if bounds.ndim > 1: # type: ignore + raise ValueError("Bounds must be 1D array or scalar.") + if bounds.shape[0] != n_features: # type: ignore + raise ValueError("Bounds must be the same length as X.shape[1].") + + return bounds + + +def check_inequality_constraints( + A_ineq: typing.Optional[np.ndarray], + b_ineq: typing.Optional[np.ndarray], + n_features: int, + dtype, +) -> tuple[typing.Union[None, np.ndarray], typing.Union[None, np.ndarray]]: + """Check that the inequality constraints are well-defined.""" + if A_ineq is None or b_ineq is None: + return None, None + else: + A_ineq = skl.utils.check_array( + A_ineq, + accept_sparse=False, + ensure_2d=True, + dtype=dtype, + copy=True, + **{keyword_finiteness: False}, + ) + b_ineq = skl.utils.check_array( + b_ineq, + accept_sparse=False, + ensure_2d=False, + dtype=dtype, + copy=True, + **{keyword_finiteness: False}, + ) + if A_ineq.shape[1] != n_features: # type: ignore + raise ValueError("A_ineq must have same number of columns as X.") + if A_ineq.shape[0] != b_ineq.shape[0]: # type: ignore + raise ValueError("A_ineq and b_ineq must have same number of rows.") + if b_ineq.ndim > 1: # type: ignore + raise ValueError("b_ineq must be 1D array.") + return A_ineq, b_ineq + + +def check_offset( + offset: typing.Optional[typing.Union[VectorLike, float]], n_rows: int, dtype +) -> typing.Optional[np.ndarray]: + """ + Unlike weights, if the offset is ``None``, it can stay ``None``, so we only + need to validate it when it is not. + """ + if offset is None: + return None + if np.isscalar(offset): + return np.full(n_rows, offset) + + offset = skl.utils.check_array( + offset, + accept_sparse=False, + ensure_2d=False, + dtype=dtype, + **{keyword_finiteness: True}, + ) + + offset = typing.cast(np.ndarray, offset) + + if offset.ndim > 1: # type: ignore + raise ValueError("Offsets must be 1D array or scalar.") + if offset.shape[0] != n_rows: # type: ignore + raise ValueError("Offsets must have the same length as y.") + + return offset + + +def check_weights( + sample_weight: typing.Optional[typing.Union[float, VectorLike]], + n_samples: int, + dtype, + force_all_finite: bool = True, +) -> np.ndarray: + """Check that sample weights are non-negative and have the right shape.""" + if sample_weight is None: + return np.ones(n_samples, dtype=dtype) + if np.isscalar(sample_weight): + if sample_weight <= 0: # type: ignore + raise ValueError("Sample weights must be non-negative.") + return np.full(n_samples, sample_weight, dtype=dtype) + + sample_weight = skl.utils.check_array( + sample_weight, + accept_sparse=False, + ensure_2d=False, + dtype=[np.float64, np.float32], + **{keyword_finiteness: force_all_finite}, + ) + + if sample_weight.ndim > 1: # type: ignore + raise ValueError("Sample weights must be 1D array or scalar.") + if sample_weight.shape[0] != n_samples: # type: ignore + raise ValueError("Sample weights must have the same length as y.") + if np.any(sample_weight < 0): # type: ignore + raise ValueError("Sample weights must be non-negative.") + if np.sum(sample_weight) == 0: # type: ignore + raise ValueError("Sample weights must have at least one positive element.") + + return sample_weight # type: ignore diff --git a/src/glum_benchmarks/README.md b/src/glum_benchmarks/README.md index c3ac43e23..51ad118aa 100644 --- a/src/glum_benchmarks/README.md +++ b/src/glum_benchmarks/README.md @@ -2,7 +2,7 @@ ![CI](https://github.com/Quantco/glm_benchmarks/workflows/CI/badge.svg) -Python package to benchmark GLM implementations. +Python package to benchmark GLM implementations. ## Running the benchmarks @@ -10,7 +10,7 @@ After installing the package, you should have two CLI tools: `glm_benchmarks_run To run the full benchmarking suite, just run `glm_benchmarks_run` with no flags. This will probably take a very long time. -For a more advanced example: `glm_benchmarks_run --problem_name narrow-insurance-no-weights-l2-poisson --library_name glum --storage dense --num_rows 1000 --output_dir mydatadirname` will run just the first 1000 rows of the `narrow-insurance-no-weights-l2-poisson` problem through the `glum` library and save the output to `mydatadirname`. This demonstrates several capabilities that will speed development when you just want to run a subset of either data or problems or libraries. +For a more advanced example: `glm_benchmarks_run --problem_name narrow-insurance-no-weights-l2-poisson --library_name glum --storage dense --num_rows 1000 --output_dir mydatadirname` will run just the first 1000 rows of the `narrow-insurance-no-weights-l2-poisson` problem through the `glum` library and save the output to `mydatadirname`. This demonstrates several capabilities that will speed development when you just want to run a subset of either data or problems or libraries. Demonstrating the command above: ``` @@ -20,7 +20,7 @@ e-no-weights-l2-poisson --library_name glum --storage dense --num_rows 1000 --ou running problem=narrow-insurance-no-weights-l2-poisson library=glum Diagnostics: convergence n_cycles iteration_runtime intercept -n_iter +n_iter 0 1.444101e+00 0 0.001196 -1.843114 1 5.008199e-01 1 0.009937 -1.843114 2 8.087132e-02 2 0.001981 -2.311497 @@ -33,13 +33,13 @@ ran in 0.045558929443359375 The `--problem_name` and `--library_name` flags take comma separated lists. This mean that if you want to run both `glum` and `r-glmnet`, you could run `glm_benchmarks_run --library_name glum,r-glmnet`. -The `glm_benchmarks_analyze` tool produces a dataframe comparing the correct and runtime of several runs/libraries. `glm_benchmarks_analyze` accepts an almost identical range of command line parameters as `glm_benchmarks_run`. You can use these CLI parameters to filter which problems and runs you would like to compare. +The `glm_benchmarks_analyze` tool produces a dataframe comparing the correct and runtime of several runs/libraries. `glm_benchmarks_analyze` accepts an almost identical range of command line parameters as `glm_benchmarks_run`. You can use these CLI parameters to filter which problems and runs you would like to compare. For example: ``` (glum) ➜ glum git:(master) ✗ glm_benchmarks_analyze --problem_name narrow-insurance-no-weights-l2-poisson --library_name glum --storage dense --num_rows 1000 --output_dir mydatadirname --cols intercept,runtime,n_iter library_name intercept runtime n_iter -problem_name num_rows regularization_strength offset +problem_name num_rows regularization_strength offset narrow-insurance-no-weights-l2-poisson 1000 0.001 False glum -3.3194 0.0456 5 ``` diff --git a/tests/glm/golden_master/benchmark_gm.json b/tests/glm/golden_master/benchmark_gm.json index f644f68e4..96c614ff9 100644 --- a/tests/glm/golden_master/benchmark_gm.json +++ b/tests/glm/golden_master/benchmark_gm.json @@ -15350,4 +15350,4 @@ ], "intercept": 432.16074911954195 } -} \ No newline at end of file +} diff --git a/tests/glm/golden_master/simulation_gm.json b/tests/glm/golden_master/simulation_gm.json index 660413de6..139c83d43 100644 --- a/tests/glm/golden_master/simulation_gm.json +++ b/tests/glm/golden_master/simulation_gm.json @@ -6813,4 +6813,4 @@ "n_iter_": 4 } } -} \ No newline at end of file +} diff --git a/tests/glm/test_distribution.py b/tests/glm/test_distribution.py index 5ce4b9d21..82bce63b8 100644 --- a/tests/glm/test_distribution.py +++ b/tests/glm/test_distribution.py @@ -14,9 +14,10 @@ PoissonDistribution, TweedieDistribution, ) -from glum._glm import GeneralizedLinearRegressor, get_family +from glum._glm import get_family +from glum._glm_regressor import GeneralizedLinearRegressor +from glum._linalg import _safe_sandwich_dot from glum._link import IdentityLink, LogitLink, LogLink, TweedieLink -from glum._util import _safe_sandwich_dot @pytest.mark.parametrize( diff --git a/tests/glm/test_formula.py b/tests/glm/test_formula.py new file mode 100644 index 000000000..f5bf688b7 --- /dev/null +++ b/tests/glm/test_formula.py @@ -0,0 +1,316 @@ +import formulaic +import numpy as np +import pandas as pd +import pytest +import statsmodels.api as sm +import statsmodels.formula.api as smf + +from glum._formula import parse_formula +from glum._glm_regressor import GeneralizedLinearRegressor + + +@pytest.fixture +def get_mixed_data(): + nrow = 10 + np.random.seed(0) + return pd.DataFrame( + { + "y": np.random.rand(nrow), + "x1": np.random.rand(nrow), + "x2": np.random.rand(nrow), + "c1": np.random.choice(["a", "b", "c"], nrow), + "c2": np.random.choice(["d", "e"], nrow), + } + ) + + +@pytest.mark.parametrize( + "input, expected", + [ + pytest.param( + "y ~ x1 + x2", + (["y"], ["1", "x1", "x2"]), + id="implicit_intercept", + ), + pytest.param( + "y ~ x1 + x2 + 1", + (["y"], ["1", "x1", "x2"]), + id="explicit_intercept", + ), + pytest.param( + "y ~ x1 + x2 - 1", + (["y"], ["x1", "x2"]), + id="no_intercept", + ), + pytest.param( + "y ~ ", + (["y"], ["1"]), + id="empty_rhs", + ), + ], +) +def test_parse_formula(input, expected): + lhs_exp, rhs_exp = expected + lhs, rhs = parse_formula(input) + assert list(lhs) == lhs_exp + assert list(rhs) == rhs_exp + + formula = formulaic.Formula(input) + lhs, rhs = parse_formula(formula) + assert list(lhs) == lhs_exp + assert list(rhs) == rhs_exp + + +@pytest.mark.parametrize( + "input, error", + [ + pytest.param("y1 + y2 ~ x1 + x2", ValueError, id="multiple_lhs"), + pytest.param([["y"], ["x1", "x2"]], TypeError, id="wrong_type"), + ], +) +def test_parse_formula_invalid(input, error): + with pytest.raises(error): + parse_formula(input) + + +@pytest.mark.parametrize( + "formula", + [ + pytest.param("y ~ x1 + x2", id="numeric"), + pytest.param("y ~ c1", id="categorical"), + pytest.param("y ~ c1 * c2", id="categorical_interaction"), + pytest.param("y ~ x1 + x2 + c1 + c2", id="numeric_categorical"), + pytest.param("y ~ x1 * c1 * c2", id="numeric_categorical_interaction"), + ], +) +@pytest.mark.parametrize( + "drop_first", [True, False], ids=["drop_first", "no_drop_first"] +) +@pytest.mark.parametrize( + "fit_intercept", [True, False], ids=["intercept", "no_intercept"] +) +def test_formula(get_mixed_data, formula, drop_first, fit_intercept): + """Model with formula and model with externally constructed model matrix should + match. + """ + data = get_mixed_data + + model_formula = GeneralizedLinearRegressor( + family="normal", + drop_first=drop_first, + formula=formula, + fit_intercept=fit_intercept, + categorical_format="{name}[T.{category}]", + alpha=1.0, + ).fit(data) + + if fit_intercept: + # full rank check must consider presence of intercept + y_ext, X_ext = formulaic.model_matrix( + formula, + data, + ensure_full_rank=drop_first, + materializer=formulaic.materializers.PandasMaterializer, + ) + X_ext = X_ext.drop(columns="Intercept") + else: + y_ext, X_ext = formulaic.model_matrix( + formula + "-1", + data, + ensure_full_rank=drop_first, + materializer=formulaic.materializers.PandasMaterializer, + ) + y_ext = y_ext.iloc[:, 0] + + model_ext = GeneralizedLinearRegressor( + family="normal", + drop_first=drop_first, + fit_intercept=fit_intercept, + categorical_format="{name}[T.{category}]", + alpha=1.0, + ).fit(X_ext, y_ext) + + np.testing.assert_almost_equal(model_ext.coef_, model_formula.coef_) + + +def test_formula_explicit_intercept(get_mixed_data): + data = get_mixed_data + + with pytest.raises(ValueError, match="The formula sets the intercept to False"): + GeneralizedLinearRegressor( + family="normal", + formula="y ~ x1 - 1", + fit_intercept=True, + ).fit(data) + + +@pytest.mark.parametrize( + "formula, feature_names, term_names", + [ + pytest.param("y ~ x1 + x2", ["x1", "x2"], ["x1", "x2"], id="numeric"), + pytest.param( + "y ~ c1", ["c1[T.a]", "c1[T.b]", "c1[T.c]"], 3 * ["c1"], id="categorical" + ), + pytest.param( + "y ~ x1 : c1", + ["x1:c1[T.a]", "x1:c1[T.b]", "x1:c1[T.c]"], + 3 * ["x1:c1"], + id="interaction", + ), + pytest.param( + "y ~ poly(x1, 3)", + ["poly(x1, 3)[1]", "poly(x1, 3)[2]", "poly(x1, 3)[3]"], + 3 * ["poly(x1, 3)"], + id="function", + ), + ], +) +def test_formula_names_formulaic_style( + get_mixed_data, formula, feature_names, term_names +): + data = get_mixed_data + model_formula = GeneralizedLinearRegressor( + family="normal", + drop_first=False, + formula=formula, + categorical_format="{name}[T.{category}]", + interaction_separator=":", + alpha=1.0, + ).fit(data) + + np.testing.assert_array_equal(model_formula.feature_names_, feature_names) + np.testing.assert_array_equal(model_formula.term_names_, term_names) + + +@pytest.mark.parametrize( + "formula, feature_names, term_names", + [ + pytest.param("y ~ x1 + x2", ["x1", "x2"], ["x1", "x2"], id="numeric"), + pytest.param( + "y ~ c1", ["c1__a", "c1__b", "c1__c"], 3 * ["c1"], id="categorical" + ), + pytest.param( + "y ~ x1 : c1", + ["x1__x__c1__a", "x1__x__c1__b", "x1__x__c1__c"], + 3 * ["x1:c1"], + id="interaction", + ), + pytest.param( + "y ~ poly(x1, 3)", + ["poly(x1, 3)[1]", "poly(x1, 3)[2]", "poly(x1, 3)[3]"], + 3 * ["poly(x1, 3)"], + id="function", + ), + ], +) +def test_formula_names_old_glum_style( + get_mixed_data, formula, feature_names, term_names +): + data = get_mixed_data + model_formula = GeneralizedLinearRegressor( + family="normal", + drop_first=False, + formula=formula, + categorical_format="{name}__{category}", + interaction_separator="__x__", + alpha=1.0, + ).fit(data) + + np.testing.assert_array_equal(model_formula.feature_names_, feature_names) + np.testing.assert_array_equal(model_formula.term_names_, term_names) + + +@pytest.mark.parametrize( + "formula", + [ + pytest.param("y ~ x1 + x2", id="numeric"), + pytest.param("y ~ c1", id="categorical"), + pytest.param("y ~ c1 * c2", id="categorical_interaction"), + ], +) +@pytest.mark.parametrize( + "fit_intercept", [True, False], ids=["intercept", "no_intercept"] +) +def test_formula_against_smf(get_mixed_data, formula, fit_intercept): + data = get_mixed_data + model_formula = GeneralizedLinearRegressor( + family="normal", + drop_first=True, + formula=formula, + fit_intercept=fit_intercept, + ).fit(data) + + if fit_intercept: + beta_formula = np.concatenate([[model_formula.intercept_], model_formula.coef_]) + else: + beta_formula = model_formula.coef_ + + formula_smf = formula + "- 1" if not fit_intercept else formula + model_smf = smf.glm(formula_smf, data, family=sm.families.Gaussian()).fit() + + np.testing.assert_almost_equal(beta_formula, model_smf.params) + + +def test_formula_context(get_mixed_data): + data = get_mixed_data + x_context = np.arange(len(data), dtype=float) # noqa: F841 + formula = "y ~ x1 + x2 + x_context" + + model_formula = GeneralizedLinearRegressor( + family="normal", + drop_first=True, + formula=formula, + fit_intercept=True, + ) + # default is to add nothing to context + with pytest.raises(formulaic.errors.FactorEvaluationError): + model_formula.fit(data) + + # set context to 0 to capture calling scope + model_formula = GeneralizedLinearRegressor( + family="normal", + drop_first=True, + formula=formula, + fit_intercept=True, + ).fit(data, context=0) + + model_smf = smf.glm(formula, data, family=sm.families.Gaussian()).fit() + + np.testing.assert_almost_equal( + np.concatenate([[model_formula.intercept_], model_formula.coef_]), + model_smf.params, + ) + np.testing.assert_almost_equal( + model_formula.predict(data, context=0), model_smf.predict(data) + ) + + +@pytest.mark.parametrize( + "formula", + [ + pytest.param("y ~ x1 + x2", id="numeric"), + pytest.param("y ~ c1", id="categorical"), + pytest.param("y ~ c1 * c2", id="categorical_interaction"), + ], +) +@pytest.mark.parametrize( + "fit_intercept", [True, False], ids=["intercept", "no_intercept"] +) +def test_formula_predict(get_mixed_data, formula, fit_intercept): + data = get_mixed_data + data_unseen = data.copy() + data_unseen.loc[data_unseen["c1"] == "b", "c1"] = "c" + model_formula = GeneralizedLinearRegressor( + family="normal", + drop_first=True, + formula=formula, + fit_intercept=fit_intercept, + ).fit(data) + + formula_smf = formula + "- 1" if not fit_intercept else formula + model_smf = smf.glm(formula_smf, data, family=sm.families.Gaussian()).fit() + + yhat_formula = model_formula.predict(data_unseen) + yhat_smf = model_smf.predict(data_unseen) + + np.testing.assert_almost_equal(yhat_formula, yhat_smf) diff --git a/tests/glm/test_glm_base.py b/tests/glm/test_glm_base.py new file mode 100644 index 000000000..4cf20c917 --- /dev/null +++ b/tests/glm/test_glm_base.py @@ -0,0 +1,554 @@ +from typing import Any, Union + +import numpy as np +import pandas as pd +import pytest +import sklearn as skl +import sklearn.utils.estimator_checks +from scipy import sparse + +from glum._distribution import ( + BinomialDistribution, + ExponentialDispersionModel, + GammaDistribution, + InverseGaussianDistribution, + NegativeBinomialDistribution, + NormalDistribution, + PoissonDistribution, +) +from glum._glm_cv import GeneralizedLinearRegressorCV +from glum._glm_regressor import GeneralizedLinearRegressor +from glum._linalg import is_pos_semidef +from glum._link import IdentityLink, LogitLink, LogLink + +GLM_SOLVERS = ["irls-ls", "lbfgs", "irls-cd", "trust-constr"] + +estimators = [ + (GeneralizedLinearRegressor, {"alpha": 1.0}), + (GeneralizedLinearRegressorCV, {"n_alphas": 2}), +] + + +def get_small_x_y( + estimator: Union[GeneralizedLinearRegressor, GeneralizedLinearRegressorCV], +) -> tuple[np.ndarray, np.ndarray]: + if isinstance(estimator, GeneralizedLinearRegressor): + n_rows = 2 + else: + n_rows = 10 + x = np.ones((n_rows, 1), dtype=int) + y = np.array([0, 1] * (n_rows // 2)) * 0.5 + return x, y + + +@pytest.fixture(scope="module") +def regression_data(): + X, y = skl.datasets.make_regression( + n_samples=107, n_features=10, n_informative=80, noise=0.5, random_state=2 + ) + return X, y + + +@pytest.fixture +def y(): + """Get values for y that are in range of all distributions.""" + return np.array([0.1, 0.5]) + + +@pytest.fixture +def X(): + return np.array([[1], [2]]) + + +@pytest.mark.parametrize("estimator, kwargs", estimators) +def test_sample_weights_validation(estimator, kwargs): + """Test the raised errors in the validation of sample_weight.""" + # scalar value but not positive + X, y = get_small_x_y(estimator) + sample_weight: Any = 0 + glm = estimator(fit_intercept=False, **kwargs) + with pytest.raises(ValueError, match="weights must be non-negative"): + glm.fit(X, y, sample_weight) + + # Positive weights are accepted + glm.fit(X, y, sample_weight=1) + + # 2d array + sample_weight = [[0]] + with pytest.raises(ValueError, match="must be 1D array or scalar"): + glm.fit(X, y, sample_weight) + + # 1d but wrong length + sample_weight = [1, 0] + with pytest.raises(ValueError, match="weights must have the same length as y"): + glm.fit(X, y, sample_weight) + + # 1d but only zeros (sum not greater than 0) + sample_weight = [0, 0] + X = [[0], [1]] + y = [1, 2] + with pytest.raises(ValueError, match="must have at least one positive element"): + glm.fit(X, y, sample_weight) + + # 5. 1d but with a negative value + sample_weight = [2, -1] + with pytest.raises(ValueError, match="weights must be non-negative"): + glm.fit(X, y, sample_weight) + + +@pytest.mark.parametrize("estimator, kwargs", estimators) +def test_offset_validation(estimator, kwargs): + X, y = get_small_x_y(estimator) + glm = estimator(fit_intercept=False, **kwargs) + + # Negatives are accepted (makes sense for log link) + glm.fit(X, y, offset=-1) + + # Arrays of the right shape are accepted + glm.fit(X, y, offset=y.copy()) + + # 2d array + with pytest.raises(ValueError, match="must be 1D array or scalar"): + glm.fit(X, y, offset=np.zeros_like(X)) + + # 1d but wrong length + with pytest.raises(ValueError, match="must have the same length as y"): + glm.fit(X, y, offset=[1, 0]) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +def test_tol_validation_errors(estimator): + X, y = get_small_x_y(estimator) + + glm = estimator(gradient_tol=-0.1) + with pytest.raises(ValueError, match="Tolerance for the gradient stopping"): + glm.fit(X, y) + + glm = estimator(step_size_tol=-0.1) + with pytest.raises(ValueError, match="Tolerance for the step-size stopping"): + glm.fit(X, y) + + +@pytest.mark.parametrize("estimator, kwargs", estimators) +@pytest.mark.parametrize( + "tol_kws", + [ + {}, + {"step_size_tol": 1}, + {"step_size_tol": None}, + {"gradient_tol": 1}, + {"gradient_tol": 1, "step_size_tol": 1}, + ], +) +def test_tol_validation_no_error(estimator, kwargs, tol_kws): + X, y = get_small_x_y(estimator) + glm = estimator(**tol_kws, **kwargs) + glm.fit(X, y) + + +@pytest.mark.parametrize("estimator, kwargs", estimators) +@pytest.mark.parametrize("solver", ["auto", "irls-cd", "trust-constr"]) +@pytest.mark.parametrize("gradient_tol", [None, 1]) +def test_gradient_tol_setting(estimator, kwargs, solver, gradient_tol): + X, y = get_small_x_y(estimator) + glm = estimator(solver=solver, gradient_tol=gradient_tol, **kwargs) + glm.fit(X, y) + + if gradient_tol is None: + if solver == "trust-constr": + gradient_tol = 1e-8 + else: + gradient_tol = 1e-4 + + np.testing.assert_allclose(gradient_tol, glm._gradient_tol) + + +# TODO: something for CV regressor +@pytest.mark.parametrize( + "f, fam", + [ + ("gaussian", NormalDistribution()), + ("normal", NormalDistribution()), + ("poisson", PoissonDistribution()), + ("gamma", GammaDistribution()), + ("inverse.gaussian", InverseGaussianDistribution()), + ("binomial", BinomialDistribution()), + ("negative.binomial", NegativeBinomialDistribution()), + ], +) +def test_glm_family_argument(f, fam, y, X): + """Test GLM family argument set as string.""" + glm = GeneralizedLinearRegressor(family=f).fit(X, y) + assert isinstance(glm._family_instance, fam.__class__) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +def test_glm_family_argument_invalid_input(estimator): + X, y = get_small_x_y(estimator) + glm = estimator(family="not a family", fit_intercept=False) + with pytest.raises(ValueError, match="family must be"): + glm.fit(X, y) + + +@pytest.mark.parametrize("estimator, kwargs", estimators) +@pytest.mark.parametrize("family", ExponentialDispersionModel.__subclasses__()) +def test_glm_family_argument_as_exponential_dispersion_model(estimator, kwargs, family): + X, y = get_small_x_y(estimator) + glm = estimator(family=family(), **kwargs) + glm.fit(X, np.where(y > family().lower_bound, y, y.max() / 2)) + + +@pytest.mark.parametrize( + "link_func, link", + [("identity", IdentityLink()), ("log", LogLink()), ("logit", LogitLink())], +) +def test_glm_link_argument(link_func, link, y, X): + """Test GLM link argument set as string.""" + glm = GeneralizedLinearRegressor(family="normal", link=link_func).fit(X, y) + assert isinstance(glm._link_instance, link.__class__) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +def test_glm_link_argument_invalid_input(estimator): + X, y = get_small_x_y(estimator) + glm = estimator(family="normal", link="not a link") + with pytest.raises(ValueError, match="link must be"): + glm.fit(X, y) + + +@pytest.mark.parametrize("alpha", ["not a number", -4.2]) +def test_glm_alpha_argument(alpha, y, X): + """Test GLM for invalid alpha argument.""" + glm = GeneralizedLinearRegressor(family="normal", alpha=alpha) + with pytest.raises(ValueError, match="Penalty term must be a non-negative"): + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize("l1_ratio", ["not a number", -4.2, 1.1]) +def test_glm_l1_ratio_argument(estimator, l1_ratio): + """Test GLM for invalid l1_ratio argument.""" + X, y = get_small_x_y(estimator) + glm = estimator(family="normal", l1_ratio=l1_ratio) + with pytest.raises(ValueError, match="l1_ratio must be a number in interval.*0, 1"): + glm.fit(X, y) + + +def test_glm_ratio_argument_array(): + X, y = get_small_x_y(GeneralizedLinearRegressor) + glm = GeneralizedLinearRegressor(family="normal", l1_ratio=[1]) + with pytest.raises(ValueError, match="l1_ratio must be a number in interval.*0, 1"): + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize("P1", [["a string", "a string"], [1, [2]], [1, 2, 3], [-1]]) +def test_glm_P1_argument(estimator, P1, y, X): + """Test GLM for invalid P1 argument.""" + glm = estimator(P1=P1, l1_ratio=0.5, check_input=True) + with pytest.raises((ValueError, TypeError)): + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize( + "P2", ["a string", [1, 2, 3], [[2, 3]], sparse.csr_matrix([1, 2, 3]), [-1]] +) +def test_glm_P2_argument(estimator, P2, y, X): + """Test GLM for invalid P2 argument.""" + glm = estimator(P2=P2, check_input=True) + with pytest.raises(ValueError): + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +def test_glm_P2_positive_semidefinite(estimator): + """Test GLM for a positive semi-definite P2 argument.""" + n_samples, n_features = 10, 2 + y = np.arange(n_samples) + X = np.zeros((n_samples, n_features)) + + # negative definite matrix + P2 = np.array([[1, 2], [2, 1]]) + glm = estimator(P2=P2, fit_intercept=False, check_input=True) + with pytest.raises(ValueError, match="P2 must be positive semi-definite"): + glm.fit(X, y) + + P2 = sparse.csr_matrix(P2) + glm = estimator(P2=P2, fit_intercept=False, check_input=True) + with pytest.raises(ValueError, match="P2 must be positive semi-definite"): + glm.fit(X, y) + + +def test_positive_semidefinite(): + """Test GLM for a positive semi-definite P2 argument.""" + # negative definite matrix + P2 = np.array([[1, 2], [2, 1]]) + assert not is_pos_semidef(P2) + + P2 = sparse.csr_matrix(P2) + assert not is_pos_semidef(P2) + + assert is_pos_semidef(np.eye(2)) + assert is_pos_semidef(sparse.eye(2)) + + +def test_P1_P2_expansion_with_categoricals(): + rng = np.random.default_rng(42) + X = pd.DataFrame( + data={ + "dense": np.linspace(0, 10, 60), + "cat": pd.Categorical(rng.integers(5, size=60)), + } + ) + y = rng.normal(size=60) + + mdl1 = GeneralizedLinearRegressor( + l1_ratio=0.01, + P1=[1, 2, 2, 2, 2, 2], + P2=[2, 1, 1, 1, 1, 1], + ) + mdl1.fit(X, y) + + mdl2 = GeneralizedLinearRegressor( + l1_ratio=0.01, + P1=[1, 2], + P2=[2, 1], + ) + mdl2.fit(X, y) + np.testing.assert_allclose(mdl1.coef_, mdl2.coef_) + + mdl2 = GeneralizedLinearRegressor( + l1_ratio=0.01, P1=[1, 2], P2=sparse.diags([2, 1, 1, 1, 1, 1]) + ) + mdl2.fit(X, y) + np.testing.assert_allclose(mdl1.coef_, mdl2.coef_) + + +def test_P1_P2_expansion_with_categoricals_missings(): + rng = np.random.default_rng(42) + X = pd.DataFrame( + data={ + "dense": np.linspace(0, 10, 60), + "cat": pd.Categorical(rng.integers(5, size=60)).remove_categories(0), + } + ) + y = rng.normal(size=60) + + mdl1 = GeneralizedLinearRegressor( + alpha=1.0, + l1_ratio=0.01, + P1=[1, 2, 2, 2, 2, 2], + P2=[2, 1, 1, 1, 1, 1], + cat_missing_method="convert", + ) + mdl1.fit(X, y) + + mdl2 = GeneralizedLinearRegressor( + alpha=1.0, + l1_ratio=0.01, + P1=[1, 2], + P2=[2, 1], + cat_missing_method="convert", + ) + mdl2.fit(X, y) + np.testing.assert_allclose(mdl1.coef_, mdl2.coef_) + + mdl3 = GeneralizedLinearRegressor( + alpha=1.0, + l1_ratio=0.01, + P1=[1, 2], + P2=sparse.diags([2, 1, 1, 1, 1, 1]), + cat_missing_method="convert", + ) + mdl3.fit(X, y) + np.testing.assert_allclose(mdl1.coef_, mdl3.coef_) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize("fit_intercept", ["not bool", 1, 0, [True]]) +def test_glm_fit_intercept_argument(estimator, fit_intercept): + """Test GLM for invalid fit_intercept argument.""" + X, y = get_small_x_y(estimator) + glm = estimator(fit_intercept=fit_intercept) + with pytest.raises(TypeError, match="fit_intercept must be bool"): + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize( + "solver, l1_ratio", + [ + ("not a solver", 0), + (1, 0), + ([1], 0), + ("irls-ls", 0.5), + ("lbfgs", 0.5), + ("trust-constr", 0.5), + ], +) +def test_glm_solver_argument(estimator, solver, l1_ratio, y, X): + """Test GLM for invalid solver argument.""" + kwargs = {"solver": solver, "l1_ratio": l1_ratio} + if estimator == GeneralizedLinearRegressor: + kwargs["alpha"] = 1.0 + glm = estimator(**kwargs) + with pytest.raises(ValueError): + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize("max_iter", ["not a number", 0, -1, 5.5, [1]]) +def test_glm_max_iter_argument(estimator, max_iter): + """Test GLM for invalid max_iter argument.""" + X, y = get_small_x_y(estimator) + glm = estimator(max_iter=max_iter) + with pytest.raises(ValueError, match="must be a positive integer"): + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize("tol_param", ["gradient_tol", "step_size_tol"]) +@pytest.mark.parametrize("tol", ["not a number", 0, -1.0, [1e-3]]) +def test_glm_tol_argument(estimator, tol_param, tol): + """Test GLM for invalid tol argument.""" + X, y = get_small_x_y(estimator) + glm = estimator(**{tol_param: tol}) + with pytest.raises(ValueError, match="stopping criteria must be positive"): + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize("warm_start", ["not bool", 1, 0, [True]]) +def test_glm_warm_start_argument(estimator, warm_start): + """Test GLM for invalid warm_start argument.""" + X, y = get_small_x_y(estimator) + glm = estimator(warm_start=warm_start) + with pytest.raises(TypeError, match="warm_start must be bool"): + glm.fit(X, y) + + +# https://github.com/Quantco/glum/issues/645 +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +def test_glm_warm_start_with_constant_column(estimator): + X, y = skl.datasets.make_regression() + X[:, 0] = 0 + kwargs = {"warm_start": True} + if estimator == GeneralizedLinearRegressor: + kwargs["alpha"] = 1.0 + glm = estimator(**kwargs) + glm.fit(X, y) + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize( + "start_params", ["not a start_params", ["zero"], [0, 0, 0], [[0, 0]], ["a", "b"]] +) +def test_glm_start_params_argument(estimator, start_params, y, X): + """Test GLM for invalid start_params argument.""" + glm = estimator(start_params=start_params) + with pytest.raises(ValueError): + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize("selection", ["not a selection", 1, 0, ["cyclic"]]) +def test_glm_selection_argument(estimator, selection): + """Test GLM for invalid selection argument.""" + X, y = get_small_x_y(estimator) + glm = estimator(selection=selection) + with pytest.raises(ValueError, match="argument selection must be"): + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize("random_state", ["a string", 0.5, [0]]) +def test_glm_random_state_argument(estimator, random_state): + """Test GLM for invalid random_state argument.""" + X, y = get_small_x_y(estimator) + glm = estimator(random_state=random_state) + with pytest.raises(ValueError, match="cannot be used to seed"): + glm.fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize("copy_X", ["not bool", 1, 0, [True]]) +def test_glm_copy_X_argument_invalid(estimator, copy_X): + """Test GLM for invalid copy_X arguments.""" + X, y = get_small_x_y(estimator) + glm = estimator(copy_X=copy_X) + with pytest.raises(TypeError, match="copy_X must be None or bool"): + glm.fit(X, y) + + +def test_glm_copy_X_input_needs_conversion(): + y = np.array([1.0]) + # If X is of int dtype, it needs to be copied + X = np.array([[1]]) + glm = GeneralizedLinearRegressor(copy_X=False) + # should raise an error + with pytest.raises(ValueError, match="copy_X"): + glm.fit(X, y) + # should be OK with copy_X = None or copy_X = True + GeneralizedLinearRegressor(copy_X=None).fit(X, y) + GeneralizedLinearRegressor(copy_X=True).fit(X, y) + + +@pytest.mark.parametrize( + "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] +) +@pytest.mark.parametrize("check_input", ["not bool", 1, 0, [True]]) +def test_glm_check_input_argument(estimator, check_input): + """Test GLM for invalid check_input argument.""" + X, y = get_small_x_y(estimator) + glm = estimator(check_input=check_input) + with pytest.raises(TypeError, match="check_input must be bool"): + glm.fit(X, y) + + +@pytest.mark.parametrize("estimator, kwargs", estimators) +def test_check_estimator(estimator, kwargs): + sklearn.utils.estimator_checks.check_estimator(estimator(**kwargs)) + + +@pytest.mark.parametrize( + "estimator", + [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV], +) +def test_clonable(estimator): + skl.base.clone(estimator()) diff --git a/tests/glm/test_cv_glm.py b/tests/glm/test_glm_cv.py similarity index 64% rename from tests/glm/test_cv_glm.py rename to tests/glm/test_glm_cv.py index 6d17f08c0..a8eeff310 100644 --- a/tests/glm/test_cv_glm.py +++ b/tests/glm/test_glm_cv.py @@ -1,10 +1,9 @@ import numpy as np import pandas as pd import pytest +import sklearn as skl import tabmat as tm -from scipy import sparse as sparse -from sklearn.datasets import make_regression -from sklearn.linear_model import ElasticNetCV, RidgeCV +from scipy import sparse from glum import GeneralizedLinearRegressorCV @@ -37,7 +36,7 @@ def test_normal_elastic_net_comparison(l1_ratio, fit_intercept, convert_x_fn): tol = 1e-9 n_predict = 10 - X, y, _ = make_regression( + X, y, _ = skl.datasets.make_regression( n_samples=n_samples + n_predict, n_features=n_features, n_informative=n_features - 2, @@ -51,7 +50,7 @@ def test_normal_elastic_net_comparison(l1_ratio, fit_intercept, convert_x_fn): x_arr = X if isinstance(X, np.ndarray) else X.toarray() t_arr = T if isinstance(T, np.ndarray) else T.toarray() - elastic_net = ElasticNetCV( + elastic_net = skl.linear_model.ElasticNetCV( l1_ratio=l1_ratio, n_alphas=n_alphas, fit_intercept=fit_intercept, @@ -96,7 +95,7 @@ def test_normal_ridge_comparison(fit_intercept): alphas = [1e-4] n_predict = 10 - X, y, coef = make_regression( + X, y, coef = skl.datasets.make_regression( n_samples=n_samples + n_predict, n_features=n_features, n_informative=n_features - 2, @@ -107,7 +106,9 @@ def test_normal_ridge_comparison(fit_intercept): y = y[0:n_samples] X, T = X[0:n_samples], X[n_samples:] - ridge = RidgeCV(fit_intercept=fit_intercept, cv=5, alphas=alphas).fit(X, y) + ridge = skl.linear_model.RidgeCV( + fit_intercept=fit_intercept, cv=5, alphas=alphas + ).fit(X, y) el_pred = ridge.predict(T) glm = GeneralizedLinearRegressorCV( @@ -126,6 +127,72 @@ def test_normal_ridge_comparison(fit_intercept): np.testing.assert_allclose(glm.coef_, ridge.coef_, atol=3e-6) +# TODO: different distributions +# Specify rtol since some are more accurate than others +@pytest.mark.parametrize( + "params", + [ + {"solver": "irls-ls", "rtol": 1e-6}, + {"solver": "lbfgs", "rtol": 2e-4}, + {"solver": "trust-constr", "rtol": 2e-4}, + {"solver": "irls-cd", "selection": "cyclic", "rtol": 2e-5}, + {"solver": "irls-cd", "selection": "random", "rtol": 6e-5}, + ], + ids=lambda params: ", ".join(f"{key}={val}" for key, val in params.items()), +) +@pytest.mark.parametrize("use_offset", [False, True]) +def test_solver_equivalence_cv(params, use_offset): + n_alphas = 3 + n_samples = 100 + n_features = 10 + gradient_tol = 1e-5 + + X, y = skl.datasets.make_regression( + n_samples=n_samples, n_features=n_features, random_state=2 + ) + + if use_offset: + np.random.seed(0) + offset = np.random.random(len(y)) + else: + offset = None + + est_ref = GeneralizedLinearRegressorCV( + random_state=2, + n_alphas=n_alphas, + gradient_tol=gradient_tol, + min_alpha_ratio=1e-3, + ) + est_ref.fit(X, y, offset=offset) + + est_2 = ( + GeneralizedLinearRegressorCV( + n_alphas=n_alphas, + max_iter=1000, + gradient_tol=gradient_tol, + **{k: v for k, v in params.items() if k != "rtol"}, + min_alpha_ratio=1e-3, + ) + .set_params(random_state=2) + .fit(X, y, offset=offset) + ) + + def _assert_all_close(x, y): + return np.testing.assert_allclose(x, y, rtol=params["rtol"], atol=1e-7) + + _assert_all_close(est_2.alphas_, est_ref.alphas_) + _assert_all_close(est_2.alpha_, est_ref.alpha_) + _assert_all_close(est_2.l1_ratio_, est_ref.l1_ratio_) + _assert_all_close(est_2.coef_path_, est_ref.coef_path_) + _assert_all_close(est_2.deviance_path_, est_ref.deviance_path_) + _assert_all_close(est_2.intercept_, est_ref.intercept_) + _assert_all_close(est_2.coef_, est_ref.coef_) + _assert_all_close( + skl.metrics.mean_absolute_error(est_2.predict(X), y), + skl.metrics.mean_absolute_error(est_ref.predict(X), y), + ) + + def test_formula(): """Model with formula and model with externally constructed model matrix should match. diff --git a/tests/glm/test_glm.py b/tests/glm/test_glm_regressor.py similarity index 69% rename from tests/glm/test_glm.py rename to tests/glm/test_glm_regressor.py index e37d14017..504bb1497 100644 --- a/tests/glm/test_glm.py +++ b/tests/glm/test_glm_regressor.py @@ -1,32 +1,18 @@ -# Authors: Christian Lorentzen -# -# License: BSD 3 clause import copy import warnings from typing import Any, Optional, Union -import formulaic import numpy as np import pandas as pd import pytest +import sklearn as skl import statsmodels.api as sm -import statsmodels.formula.api as smf +import statsmodels.tools import tabmat as tm -from formulaic import Formula -from numpy.testing import assert_allclose from scipy import optimize, sparse -from sklearn.base import clone -from sklearn.datasets import make_classification, make_regression -from sklearn.exceptions import ConvergenceWarning -from sklearn.linear_model import ElasticNet, LogisticRegression, Ridge -from sklearn.metrics import mean_absolute_error -from sklearn.utils.estimator_checks import check_estimator -from statsmodels.tools import eval_measures - -from glum import GeneralizedLinearRegressorCV + from glum._distribution import ( BinomialDistribution, - ExponentialDispersionModel, GammaDistribution, GeneralizedHyperbolicSecant, InverseGaussianDistribution, @@ -34,15 +20,11 @@ NormalDistribution, PoissonDistribution, TweedieDistribution, - guess_intercept, -) -from glum._glm import ( - GeneralizedLinearRegressor, - _parse_formula, - _unstandardize, - is_pos_semidef, ) -from glum._link import IdentityLink, Link, LogitLink, LogLink +from glum._glm_cv import GeneralizedLinearRegressorCV +from glum._glm_regressor import GeneralizedLinearRegressor +from glum._link import LogitLink, LogLink +from glum._utils import unstandardize GLM_SOLVERS = ["irls-ls", "lbfgs", "irls-cd", "trust-constr"] @@ -66,7 +48,7 @@ def get_small_x_y( @pytest.fixture(scope="module") def regression_data(): - X, y = make_regression( + X, y = skl.datasets.make_regression( n_samples=107, n_features=10, n_informative=80, noise=0.5, random_state=2 ) return X, y @@ -83,487 +65,6 @@ def X(): return np.array([[1], [2]]) -@pytest.mark.parametrize("estimator, kwargs", estimators) -def test_sample_weights_validation(estimator, kwargs): - """Test the raised errors in the validation of sample_weight.""" - # scalar value but not positive - X, y = get_small_x_y(estimator) - sample_weight: Any = 0 - glm = estimator(fit_intercept=False, **kwargs) - with pytest.raises(ValueError, match="weights must be non-negative"): - glm.fit(X, y, sample_weight) - - # Positive weights are accepted - glm.fit(X, y, sample_weight=1) - - # 2d array - sample_weight = [[0]] - with pytest.raises(ValueError, match="must be 1D array or scalar"): - glm.fit(X, y, sample_weight) - - # 1d but wrong length - sample_weight = [1, 0] - with pytest.raises(ValueError, match="weights must have the same length as y"): - glm.fit(X, y, sample_weight) - - # 1d but only zeros (sum not greater than 0) - sample_weight = [0, 0] - X = [[0], [1]] - y = [1, 2] - with pytest.raises(ValueError, match="must have at least one positive element"): - glm.fit(X, y, sample_weight) - - # 5. 1d but with a negative value - sample_weight = [2, -1] - with pytest.raises(ValueError, match="weights must be non-negative"): - glm.fit(X, y, sample_weight) - - -@pytest.mark.parametrize("estimator, kwargs", estimators) -def test_offset_validation(estimator, kwargs): - X, y = get_small_x_y(estimator) - glm = estimator(fit_intercept=False, **kwargs) - - # Negatives are accepted (makes sense for log link) - glm.fit(X, y, offset=-1) - - # Arrays of the right shape are accepted - glm.fit(X, y, offset=y.copy()) - - # 2d array - with pytest.raises(ValueError, match="must be 1D array or scalar"): - glm.fit(X, y, offset=np.zeros_like(X)) - - # 1d but wrong length - with pytest.raises(ValueError, match="must have the same length as y"): - glm.fit(X, y, offset=[1, 0]) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -def test_tol_validation_errors(estimator): - X, y = get_small_x_y(estimator) - - glm = estimator(gradient_tol=-0.1) - with pytest.raises(ValueError, match="Tolerance for the gradient stopping"): - glm.fit(X, y) - - glm = estimator(step_size_tol=-0.1) - with pytest.raises(ValueError, match="Tolerance for the step-size stopping"): - glm.fit(X, y) - - -@pytest.mark.parametrize("estimator, kwargs", estimators) -@pytest.mark.parametrize( - "tol_kws", - [ - {}, - {"step_size_tol": 1}, - {"step_size_tol": None}, - {"gradient_tol": 1}, - {"gradient_tol": 1, "step_size_tol": 1}, - ], -) -def test_tol_validation_no_error(estimator, kwargs, tol_kws): - X, y = get_small_x_y(estimator) - glm = estimator(**tol_kws, **kwargs) - glm.fit(X, y) - - -@pytest.mark.parametrize("estimator, kwargs", estimators) -@pytest.mark.parametrize("solver", ["auto", "irls-cd", "trust-constr"]) -@pytest.mark.parametrize("gradient_tol", [None, 1]) -def test_gradient_tol_setting(estimator, kwargs, solver, gradient_tol): - X, y = get_small_x_y(estimator) - glm = estimator(solver=solver, gradient_tol=gradient_tol, **kwargs) - glm.fit(X, y) - - if gradient_tol is None: - if solver == "trust-constr": - gradient_tol = 1e-8 - else: - gradient_tol = 1e-4 - - np.testing.assert_allclose(gradient_tol, glm._gradient_tol) - - -# TODO: something for CV regressor -@pytest.mark.parametrize( - "f, fam", - [ - ("gaussian", NormalDistribution()), - ("normal", NormalDistribution()), - ("poisson", PoissonDistribution()), - ("gamma", GammaDistribution()), - ("inverse.gaussian", InverseGaussianDistribution()), - ("binomial", BinomialDistribution()), - ("negative.binomial", NegativeBinomialDistribution()), - ], -) -def test_glm_family_argument(f, fam, y, X): - """Test GLM family argument set as string.""" - glm = GeneralizedLinearRegressor(family=f).fit(X, y) - assert isinstance(glm._family_instance, fam.__class__) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -def test_glm_family_argument_invalid_input(estimator): - X, y = get_small_x_y(estimator) - glm = estimator(family="not a family", fit_intercept=False) - with pytest.raises(ValueError, match="family must be"): - glm.fit(X, y) - - -@pytest.mark.parametrize("estimator, kwargs", estimators) -@pytest.mark.parametrize("family", ExponentialDispersionModel.__subclasses__()) -def test_glm_family_argument_as_exponential_dispersion_model(estimator, kwargs, family): - X, y = get_small_x_y(estimator) - glm = estimator(family=family(), **kwargs) - glm.fit(X, np.where(y > family().lower_bound, y, y.max() / 2)) - - -@pytest.mark.parametrize( - "link_func, link", - [("identity", IdentityLink()), ("log", LogLink()), ("logit", LogitLink())], -) -def test_glm_link_argument(link_func, link, y, X): - """Test GLM link argument set as string.""" - glm = GeneralizedLinearRegressor(family="normal", link=link_func).fit(X, y) - assert isinstance(glm._link_instance, link.__class__) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -def test_glm_link_argument_invalid_input(estimator): - X, y = get_small_x_y(estimator) - glm = estimator(family="normal", link="not a link") - with pytest.raises(ValueError, match="link must be"): - glm.fit(X, y) - - -@pytest.mark.parametrize("alpha", ["not a number", -4.2]) -def test_glm_alpha_argument(alpha, y, X): - """Test GLM for invalid alpha argument.""" - glm = GeneralizedLinearRegressor(family="normal", alpha=alpha) - with pytest.raises(ValueError, match="Penalty term must be a non-negative"): - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize("l1_ratio", ["not a number", -4.2, 1.1]) -def test_glm_l1_ratio_argument(estimator, l1_ratio): - """Test GLM for invalid l1_ratio argument.""" - X, y = get_small_x_y(estimator) - glm = estimator(family="normal", l1_ratio=l1_ratio) - with pytest.raises(ValueError, match="l1_ratio must be a number in interval.*0, 1"): - glm.fit(X, y) - - -def test_glm_ratio_argument_array(): - X, y = get_small_x_y(GeneralizedLinearRegressor) - glm = GeneralizedLinearRegressor(family="normal", l1_ratio=[1]) - with pytest.raises(ValueError, match="l1_ratio must be a number in interval.*0, 1"): - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize("P1", [["a string", "a string"], [1, [2]], [1, 2, 3], [-1]]) -def test_glm_P1_argument(estimator, P1, y, X): - """Test GLM for invalid P1 argument.""" - glm = estimator(P1=P1, l1_ratio=0.5, check_input=True) - with pytest.raises((ValueError, TypeError)): - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize( - "P2", ["a string", [1, 2, 3], [[2, 3]], sparse.csr_matrix([1, 2, 3]), [-1]] -) -def test_glm_P2_argument(estimator, P2, y, X): - """Test GLM for invalid P2 argument.""" - glm = estimator(P2=P2, check_input=True) - with pytest.raises(ValueError): - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -def test_glm_P2_positive_semidefinite(estimator): - """Test GLM for a positive semi-definite P2 argument.""" - n_samples, n_features = 10, 2 - y = np.arange(n_samples) - X = np.zeros((n_samples, n_features)) - - # negative definite matrix - P2 = np.array([[1, 2], [2, 1]]) - glm = estimator(P2=P2, fit_intercept=False, check_input=True) - with pytest.raises(ValueError, match="P2 must be positive semi-definite"): - glm.fit(X, y) - - P2 = sparse.csr_matrix(P2) - glm = estimator(P2=P2, fit_intercept=False, check_input=True) - with pytest.raises(ValueError, match="P2 must be positive semi-definite"): - glm.fit(X, y) - - -def test_positive_semidefinite(): - """Test GLM for a positive semi-definite P2 argument.""" - # negative definite matrix - P2 = np.array([[1, 2], [2, 1]]) - assert not is_pos_semidef(P2) - - P2 = sparse.csr_matrix(P2) - assert not is_pos_semidef(P2) - - assert is_pos_semidef(np.eye(2)) - assert is_pos_semidef(sparse.eye(2)) - - -def test_P1_P2_expansion_with_categoricals(): - rng = np.random.default_rng(42) - X = pd.DataFrame( - data={ - "dense": np.linspace(0, 10, 60), - "cat": pd.Categorical(rng.integers(5, size=60)), - } - ) - y = rng.normal(size=60) - - mdl1 = GeneralizedLinearRegressor( - l1_ratio=0.01, - P1=[1, 2, 2, 2, 2, 2], - P2=[2, 1, 1, 1, 1, 1], - ) - mdl1.fit(X, y) - - mdl2 = GeneralizedLinearRegressor( - l1_ratio=0.01, - P1=[1, 2], - P2=[2, 1], - ) - mdl2.fit(X, y) - np.testing.assert_allclose(mdl1.coef_, mdl2.coef_) - - mdl2 = GeneralizedLinearRegressor( - l1_ratio=0.01, P1=[1, 2], P2=sparse.diags([2, 1, 1, 1, 1, 1]) - ) - mdl2.fit(X, y) - np.testing.assert_allclose(mdl1.coef_, mdl2.coef_) - - -def test_P1_P2_expansion_with_categoricals_missings(): - rng = np.random.default_rng(42) - X = pd.DataFrame( - data={ - "dense": np.linspace(0, 10, 60), - "cat": pd.Categorical(rng.integers(5, size=60)).remove_categories(0), - } - ) - y = rng.normal(size=60) - - mdl1 = GeneralizedLinearRegressor( - alpha=1.0, - l1_ratio=0.01, - P1=[1, 2, 2, 2, 2, 2], - P2=[2, 1, 1, 1, 1, 1], - cat_missing_method="convert", - ) - mdl1.fit(X, y) - - mdl2 = GeneralizedLinearRegressor( - alpha=1.0, - l1_ratio=0.01, - P1=[1, 2], - P2=[2, 1], - cat_missing_method="convert", - ) - mdl2.fit(X, y) - np.testing.assert_allclose(mdl1.coef_, mdl2.coef_) - - mdl3 = GeneralizedLinearRegressor( - alpha=1.0, - l1_ratio=0.01, - P1=[1, 2], - P2=sparse.diags([2, 1, 1, 1, 1, 1]), - cat_missing_method="convert", - ) - mdl3.fit(X, y) - np.testing.assert_allclose(mdl1.coef_, mdl3.coef_) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize("fit_intercept", ["not bool", 1, 0, [True]]) -def test_glm_fit_intercept_argument(estimator, fit_intercept): - """Test GLM for invalid fit_intercept argument.""" - X, y = get_small_x_y(estimator) - glm = estimator(fit_intercept=fit_intercept) - with pytest.raises(TypeError, match="fit_intercept must be bool"): - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize( - "solver, l1_ratio", - [ - ("not a solver", 0), - (1, 0), - ([1], 0), - ("irls-ls", 0.5), - ("lbfgs", 0.5), - ("trust-constr", 0.5), - ], -) -def test_glm_solver_argument(estimator, solver, l1_ratio, y, X): - """Test GLM for invalid solver argument.""" - kwargs = {"solver": solver, "l1_ratio": l1_ratio} - if estimator == GeneralizedLinearRegressor: - kwargs["alpha"] = 1.0 - glm = estimator(**kwargs) - with pytest.raises(ValueError): - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize("max_iter", ["not a number", 0, -1, 5.5, [1]]) -def test_glm_max_iter_argument(estimator, max_iter): - """Test GLM for invalid max_iter argument.""" - X, y = get_small_x_y(estimator) - glm = estimator(max_iter=max_iter) - with pytest.raises(ValueError, match="must be a positive integer"): - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize("tol_param", ["gradient_tol", "step_size_tol"]) -@pytest.mark.parametrize("tol", ["not a number", 0, -1.0, [1e-3]]) -def test_glm_tol_argument(estimator, tol_param, tol): - """Test GLM for invalid tol argument.""" - X, y = get_small_x_y(estimator) - glm = estimator(**{tol_param: tol}) - with pytest.raises(ValueError, match="stopping criteria must be positive"): - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize("warm_start", ["not bool", 1, 0, [True]]) -def test_glm_warm_start_argument(estimator, warm_start): - """Test GLM for invalid warm_start argument.""" - X, y = get_small_x_y(estimator) - glm = estimator(warm_start=warm_start) - with pytest.raises(TypeError, match="warm_start must be bool"): - glm.fit(X, y) - - -# https://github.com/Quantco/glum/issues/645 -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -def test_glm_warm_start_with_constant_column(estimator): - X, y = make_regression() - X[:, 0] = 0 - kwargs = {"warm_start": True} - if estimator == GeneralizedLinearRegressor: - kwargs["alpha"] = 1.0 - glm = estimator(**kwargs) - glm.fit(X, y) - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize( - "start_params", ["not a start_params", ["zero"], [0, 0, 0], [[0, 0]], ["a", "b"]] -) -def test_glm_start_params_argument(estimator, start_params, y, X): - """Test GLM for invalid start_params argument.""" - glm = estimator(start_params=start_params) - with pytest.raises(ValueError): - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize("selection", ["not a selection", 1, 0, ["cyclic"]]) -def test_glm_selection_argument(estimator, selection): - """Test GLM for invalid selection argument.""" - X, y = get_small_x_y(estimator) - glm = estimator(selection=selection) - with pytest.raises(ValueError, match="argument selection must be"): - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize("random_state", ["a string", 0.5, [0]]) -def test_glm_random_state_argument(estimator, random_state): - """Test GLM for invalid random_state argument.""" - X, y = get_small_x_y(estimator) - glm = estimator(random_state=random_state) - with pytest.raises(ValueError, match="cannot be used to seed"): - glm.fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize("copy_X", ["not bool", 1, 0, [True]]) -def test_glm_copy_X_argument_invalid(estimator, copy_X): - """Test GLM for invalid copy_X arguments.""" - X, y = get_small_x_y(estimator) - glm = estimator(copy_X=copy_X) - with pytest.raises(TypeError, match="copy_X must be None or bool"): - glm.fit(X, y) - - -def test_glm_copy_X_input_needs_conversion(): - y = np.array([1.0]) - # If X is of int dtype, it needs to be copied - X = np.array([[1]]) - glm = GeneralizedLinearRegressor(copy_X=False) - # should raise an error - with pytest.raises(ValueError, match="copy_X"): - glm.fit(X, y) - # should be OK with copy_X = None or copy_X = True - GeneralizedLinearRegressor(copy_X=None).fit(X, y) - GeneralizedLinearRegressor(copy_X=True).fit(X, y) - - -@pytest.mark.parametrize( - "estimator", [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV] -) -@pytest.mark.parametrize("check_input", ["not bool", 1, 0, [True]]) -def test_glm_check_input_argument(estimator, check_input): - """Test GLM for invalid check_input argument.""" - X, y = get_small_x_y(estimator) - glm = estimator(check_input=check_input) - with pytest.raises(TypeError, match="check_input must be bool"): - glm.fit(X, y) - - @pytest.mark.parametrize("solver", GLM_SOLVERS) @pytest.mark.parametrize("fit_intercept", [False, True]) @pytest.mark.parametrize("offset", [None, np.array([-0.1, 0, 0.1, 0, -0.2]), 0.1]) @@ -600,7 +101,7 @@ def test_glm_identity_regression(solver, fit_intercept, offset, convert_x_fn): else: fit_coef = res.coef_ assert fit_coef.dtype.itemsize == X.dtype.itemsize - assert_allclose(fit_coef, coef, rtol=1e-6) + np.testing.assert_allclose(fit_coef, coef, rtol=1e-6) @pytest.mark.parametrize("solver", GLM_SOLVERS) @@ -756,7 +257,7 @@ def test_glm_identity_regression_categorical_data(solver, offset, convert_x_fn): np.testing.assert_almost_equal(X.toarray() if hasattr(X, "toarray") else X, x_mat) res = glm.fit(X, y, offset=offset) - assert_allclose(res.coef_, coef, rtol=1e-6) + np.testing.assert_allclose(res.coef_, coef, rtol=1e-6) @pytest.mark.parametrize( @@ -797,7 +298,7 @@ def test_glm_log_regression(family, solver, tol, fit_intercept, offset): fit_coef = np.concatenate([np.atleast_1d(res.intercept_), res.coef_]) else: fit_coef = res.coef_ - assert_allclose(fit_coef, coef, rtol=8e-6) + np.testing.assert_allclose(fit_coef, coef, rtol=8e-6) @pytest.mark.filterwarnings("ignore:The line search algorithm") @@ -814,7 +315,7 @@ def test_normal_ridge_comparison(n_samples, n_features, solver, use_offset): """ alpha = 1.0 n_predict = 10 - X, y, _ = make_regression( + X, y, _ = skl.datasets.make_regression( n_samples=n_samples + n_predict, n_features=n_features, n_informative=n_features - 2, @@ -837,7 +338,10 @@ def test_normal_ridge_comparison(n_samples, n_features, solver, use_offset): ridge_params = {"solver": "sag", "max_iter": 10000, "tol": 1e-9} # GLM has 1/(2*n) * Loss + 1/2*L2, Ridge has Loss + L2 - ridge = Ridge(alpha=alpha * n_samples, random_state=42, **ridge_params) + ridge = skl.linear_model.Ridge( + alpha=alpha * n_samples, random_state=42, **ridge_params + ) + ridge.fit(X, y if offset is None else y - offset) glm = GeneralizedLinearRegressor( @@ -853,9 +357,9 @@ def test_normal_ridge_comparison(n_samples, n_features, solver, use_offset): ) glm.fit(X, y, offset=offset) assert glm.coef_.shape == (X.shape[1],) - assert_allclose(glm.coef_, ridge.coef_, rtol=5e-5) - assert_allclose(glm.intercept_, ridge.intercept_, rtol=1e-5) - assert_allclose(glm.predict(T), ridge.predict(T), rtol=1e-4) + np.testing.assert_allclose(glm.coef_, ridge.coef_, rtol=5e-5) + np.testing.assert_allclose(glm.intercept_, ridge.intercept_, rtol=1e-5) + np.testing.assert_allclose(glm.predict(T), ridge.predict(T), rtol=1e-4) @pytest.mark.parametrize( @@ -921,11 +425,13 @@ def test_poisson_ridge(solver, tol, scale_predictors, use_sparse): def check(G): G.fit(X, y) if scale_predictors: - assert_allclose(G.intercept_, -0.21002571120839675, rtol=1e-5) - assert_allclose(G.coef_, [0.16472093, 0.27051971], rtol=1e-5) + np.testing.assert_allclose(G.intercept_, -0.21002571120839675, rtol=1e-5) + np.testing.assert_allclose(G.coef_, [0.16472093, 0.27051971], rtol=1e-5) else: - assert_allclose(G.intercept_, -0.12889386979, rtol=1e-5) - assert_allclose(G.coef_, [0.29019207995, 0.03741173122], rtol=1e-5) + np.testing.assert_allclose(G.intercept_, -0.12889386979, rtol=1e-5) + np.testing.assert_allclose( + G.coef_, [0.29019207995, 0.03741173122], rtol=1e-5 + ) check(glm) @@ -976,8 +482,8 @@ def test_poisson_ridge_bounded(scale_predictors): glm.fit(X, y) # These correct values come from glmnet. - assert_allclose(glm.intercept_, -0.13568186971946633, rtol=1e-5) - assert_allclose(glm.coef_, [0.1, 0.1], rtol=1e-5) + np.testing.assert_allclose(glm.intercept_, -0.13568186971946633, rtol=1e-5) + np.testing.assert_allclose(glm.coef_, [0.1, 0.1], rtol=1e-5) @pytest.mark.parametrize("scale_predictors", [True, False]) @@ -1013,8 +519,8 @@ def test_poisson_ridge_ineq_constrained(scale_predictors): glm.fit(X, y) # These correct values come from glmnet. - assert_allclose(glm.intercept_, -0.13568186971946633, rtol=1e-5) - assert_allclose(glm.coef_, [0.1, 0.1], rtol=1e-5) + np.testing.assert_allclose(glm.intercept_, -0.13568186971946633, rtol=1e-5) + np.testing.assert_allclose(glm.coef_, [0.1, 0.1], rtol=1e-5) def test_normal_enet(): @@ -1041,23 +547,19 @@ def test_normal_enet(): ) glm.fit(X, y) - enet = ElasticNet( - alpha=alpha, - l1_ratio=l1_ratio, - fit_intercept=True, - tol=1e-8, - copy_X=True, + enet = skl.linear_model.ElasticNet( + alpha=alpha, l1_ratio=l1_ratio, fit_intercept=True, tol=1e-8, copy_X=True ) enet.fit(X, y) - assert_allclose(glm.intercept_, enet.intercept_, rtol=2e-7) - assert_allclose(glm.coef_, enet.coef_, rtol=5e-5) + np.testing.assert_allclose(glm.intercept_, enet.intercept_, rtol=2e-7) + np.testing.assert_allclose(glm.coef_, enet.coef_, rtol=5e-5) # 2. test normal enet on sparse data X = sparse.csc_matrix(X) glm.fit(X, y) - assert_allclose(glm.intercept_, enet.intercept_, rtol=2e-7) - assert_allclose(glm.coef_, enet.coef_, rtol=5e-5) + np.testing.assert_allclose(glm.intercept_, enet.intercept_, rtol=2e-7) + np.testing.assert_allclose(glm.coef_, enet.coef_, rtol=5e-5) def test_poisson_enet(): @@ -1092,8 +594,8 @@ def test_poisson_enet(): random_state=rng, ) glm.fit(X, y) - assert_allclose(glm.intercept_, glmnet_intercept, rtol=2e-6) - assert_allclose(glm.coef_, glmnet_coef, rtol=2e-7) + np.testing.assert_allclose(glm.intercept_, glmnet_intercept, rtol=2e-6) + np.testing.assert_allclose(glm.coef_, glmnet_coef, rtol=2e-7) # test results with general optimization procedure def obj(coef): @@ -1115,9 +617,9 @@ def obj(coef): tol=1e-10, options={"maxiter": 1000, "disp": False}, ) - assert_allclose(glm.intercept_, res.x[0], rtol=5e-5) - assert_allclose(glm.coef_, res.x[1:], rtol=1e-5, atol=1e-9) - assert_allclose( + np.testing.assert_allclose(glm.intercept_, res.x[0], rtol=5e-5) + np.testing.assert_allclose(glm.coef_, res.x[1:], rtol=1e-5, atol=1e-9) + np.testing.assert_allclose( obj(np.concatenate(([glm.intercept_], glm.coef_))), res.fun, rtol=1e-8 ) @@ -1133,8 +635,8 @@ def obj(coef): selection="cyclic", ) glm.fit(X, y) - assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-4) - assert_allclose(glm.coef_, glmnet_coef, rtol=1e-4) + np.testing.assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-4) + np.testing.assert_allclose(glm.coef_, glmnet_coef, rtol=1e-4) # check warm_start, therefore start with different alpha glm = GeneralizedLinearRegressor( @@ -1153,8 +655,8 @@ def obj(coef): glm.alpha = 1 X = sparse.csr_matrix(X) glm.fit(X, y) - assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-4) - assert_allclose(glm.coef_, glmnet_coef, rtol=1e-4) + np.testing.assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-4) + np.testing.assert_allclose(glm.coef_, glmnet_coef, rtol=1e-4) def test_binomial_cloglog_enet(): @@ -1196,8 +698,8 @@ def test_binomial_cloglog_enet(): # might be closer to the truth than glmnet # In the case of unregularized results, we certainly are closer # to both statsmodels and stats::glm than glmnet is. - assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) - assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) + np.testing.assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) + np.testing.assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) # same for start_params='zero' and selection='cyclic' # with reduced precision @@ -1211,8 +713,8 @@ def test_binomial_cloglog_enet(): selection="cyclic", ) glm.fit(X, y) - assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) - assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) + np.testing.assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) + np.testing.assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) # check warm_start, therefore start with different alpha glm = GeneralizedLinearRegressor( @@ -1231,8 +733,8 @@ def test_binomial_cloglog_enet(): glm.alpha = 1 X = sparse.csr_matrix(X) glm.fit(X, y) - assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) - assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) + np.testing.assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) + np.testing.assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) @pytest.mark.parametrize("solver", ["irls-cd", "irls-ls"]) @@ -1243,7 +745,7 @@ def test_binomial_cloglog_unregularized(solver): """ n_samples = 500 rng = np.random.RandomState(42) - X, y = make_classification( + X, y = skl.datasets.make_classification( n_samples=n_samples, n_classes=2, n_features=6, @@ -1266,8 +768,8 @@ def test_binomial_cloglog_unregularized(solver): ) glum_glm.fit(X, y) - assert_allclose(glum_glm.intercept_, sm_fit.params[0], rtol=2e-5) - assert_allclose(glum_glm.coef_, sm_fit.params[1:], rtol=2e-5) + np.testing.assert_allclose(glum_glm.intercept_, sm_fit.params[0], rtol=2e-5) + np.testing.assert_allclose(glum_glm.coef_, sm_fit.params[1:], rtol=2e-5) def test_inv_gaussian_log_enet(): @@ -1305,8 +807,8 @@ def test_inv_gaussian_log_enet(): random_state=rng, ) glm.fit(X, y) - assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) - assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) + np.testing.assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) + np.testing.assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) # same for start_params='zero' and selection='cyclic' # with reduced precision @@ -1320,8 +822,8 @@ def test_inv_gaussian_log_enet(): selection="cyclic", ) glm.fit(X, y) - assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) - assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) + np.testing.assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) + np.testing.assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) # check warm_start, therefore start with different alpha glm = GeneralizedLinearRegressor( @@ -1340,8 +842,8 @@ def test_inv_gaussian_log_enet(): glm.alpha = 1 X = sparse.csr_matrix(X) glm.fit(X, y) - assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) - assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) + np.testing.assert_allclose(glm.intercept_, glmnet_intercept, rtol=1e-3) + np.testing.assert_allclose(glm.coef_, glmnet_coef, rtol=1e-3) @pytest.mark.parametrize("alpha", [0.01, 0.1, 1, 10]) @@ -1353,7 +855,7 @@ def test_binomial_enet(alpha): l1_ratio = 0.5 n_samples = 500 rng = np.random.RandomState(42) - X, y = make_classification( + X, y = skl.datasets.make_classification( n_samples=n_samples, n_classes=2, n_features=6, @@ -1362,7 +864,7 @@ def test_binomial_enet(alpha): n_repeated=0, random_state=rng, ) - log = LogisticRegression( + log = skl.linear_model.LogisticRegression( penalty="elasticnet", random_state=rng, fit_intercept=False, @@ -1385,8 +887,8 @@ def test_binomial_enet(alpha): gradient_tol=1e-7, ) glm.fit(X, y) - assert_allclose(log.intercept_[0], glm.intercept_, rtol=1e-6) - assert_allclose(log.coef_[0, :], glm.coef_, rtol=5.1e-6) + np.testing.assert_allclose(log.intercept_[0], glm.intercept_, rtol=1e-6) + np.testing.assert_allclose(log.coef_[0, :], glm.coef_, rtol=5.1e-6) @pytest.mark.parametrize( @@ -1416,78 +918,15 @@ def test_solver_equivalence(params, use_offset, regression_data): est_2.fit(X, y, offset=offset) - assert_allclose(est_2.intercept_, est_ref.intercept_, rtol=1e-4) - assert_allclose(est_2.coef_, est_ref.coef_, rtol=1e-4) - assert_allclose( - mean_absolute_error(est_2.predict(X), y), - mean_absolute_error(est_ref.predict(X), y), + np.testing.assert_allclose(est_2.intercept_, est_ref.intercept_, rtol=1e-4) + np.testing.assert_allclose(est_2.coef_, est_ref.coef_, rtol=1e-4) + np.testing.assert_allclose( + skl.metrics.mean_absolute_error(est_2.predict(X), y), + skl.metrics.mean_absolute_error(est_ref.predict(X), y), rtol=1e-4, ) -# TODO: different distributions -# Specify rtol since some are more accurate than others -@pytest.mark.parametrize( - "params", - [ - {"solver": "irls-ls", "rtol": 1e-6}, - {"solver": "lbfgs", "rtol": 2e-4}, - {"solver": "trust-constr", "rtol": 2e-4}, - {"solver": "irls-cd", "selection": "cyclic", "rtol": 2e-5}, - {"solver": "irls-cd", "selection": "random", "rtol": 6e-5}, - ], - ids=lambda params: ", ".join(f"{key}={val}" for key, val in params.items()), -) -@pytest.mark.parametrize("use_offset", [False, True]) -def test_solver_equivalence_cv(params, use_offset): - n_alphas = 3 - n_samples = 100 - n_features = 10 - gradient_tol = 1e-5 - - X, y = make_regression(n_samples=n_samples, n_features=n_features, random_state=2) - if use_offset: - np.random.seed(0) - offset = np.random.random(len(y)) - else: - offset = None - - est_ref = GeneralizedLinearRegressorCV( - random_state=2, - n_alphas=n_alphas, - gradient_tol=gradient_tol, - min_alpha_ratio=1e-3, - ) - est_ref.fit(X, y, offset=offset) - - est_2 = ( - GeneralizedLinearRegressorCV( - n_alphas=n_alphas, - max_iter=1000, - gradient_tol=gradient_tol, - **{k: v for k, v in params.items() if k != "rtol"}, - min_alpha_ratio=1e-3, - ) - .set_params(random_state=2) - .fit(X, y, offset=offset) - ) - - def _assert_all_close(x, y): - return assert_allclose(x, y, rtol=params["rtol"], atol=1e-7) - - _assert_all_close(est_2.alphas_, est_ref.alphas_) - _assert_all_close(est_2.alpha_, est_ref.alpha_) - _assert_all_close(est_2.l1_ratio_, est_ref.l1_ratio_) - _assert_all_close(est_2.coef_path_, est_ref.coef_path_) - _assert_all_close(est_2.deviance_path_, est_ref.deviance_path_) - _assert_all_close(est_2.intercept_, est_ref.intercept_) - _assert_all_close(est_2.coef_, est_ref.coef_) - _assert_all_close( - mean_absolute_error(est_2.predict(X), y), - mean_absolute_error(est_ref.predict(X), y), - ) - - @pytest.mark.parametrize("solver", GLM_SOLVERS) def test_convergence_warning(solver, regression_data): X, y = regression_data @@ -1495,7 +934,7 @@ def test_convergence_warning(solver, regression_data): est = GeneralizedLinearRegressor( solver=solver, random_state=2, max_iter=1, gradient_tol=1e-20 ) - with pytest.warns(ConvergenceWarning): + with pytest.warns(skl.exceptions.ConvergenceWarning): est.fit(X, y) @@ -1550,77 +989,24 @@ def _arrays_share_data(arr1: np.ndarray, arr2: np.ndarray) -> bool: true_std = np.sqrt(0.34) np.testing.assert_almost_equal(col_stds, true_std * col_mults) - intercept_standardized = 0.0 - coef_standardized = ( + interceptstandardized = 0.0 + coefstandardized = ( np.ones_like(col_means) if col_stds is None else copy.copy(col_stds) ) - intercept, coef = _unstandardize( + intercept, coef = unstandardize( col_means, col_stds, - intercept_standardized, - coef_standardized, + interceptstandardized, + coefstandardized, ) np.testing.assert_almost_equal(intercept, -(NC + 1) * NC / 2) if scale_predictors: np.testing.assert_almost_equal(coef, 1.0) -@pytest.mark.parametrize("estimator, kwargs", estimators) -def test_check_estimator(estimator, kwargs): - check_estimator(estimator(**kwargs)) - - -@pytest.mark.parametrize( - "estimator", - [GeneralizedLinearRegressor, GeneralizedLinearRegressorCV], -) -def test_clonable(estimator): - clone(estimator()) - - -@pytest.mark.parametrize( - "link, distribution, tol", - [ - (IdentityLink(), NormalDistribution(), 1e-4), - (LogLink(), PoissonDistribution(), 1e-4), - (LogLink(), GammaDistribution(), 1e-4), - (LogLink(), TweedieDistribution(1.5), 1e-4), - (LogLink(), TweedieDistribution(4.5), 1e-4), - (LogLink(), NormalDistribution(), 1e-4), - (LogLink(), InverseGaussianDistribution(), 1e-4), - (LogLink(), NegativeBinomialDistribution(), 1e-2), - (LogitLink(), BinomialDistribution(), 1e-2), - (IdentityLink(), GeneralizedHyperbolicSecant(), 1e-1), - ], -) -@pytest.mark.parametrize("offset", [None, np.array([0.3, -0.1, 0, 0.1]), 0.1]) -def test_get_best_intercept( - link: Link, distribution: ExponentialDispersionModel, tol: float, offset -): - y = np.array([1, 1, 1, 2], dtype=np.float64) - if isinstance(distribution, BinomialDistribution): - y -= 1 - - sample_weight = np.array([0.1, 0.2, 5, 1]) - best_intercept = guess_intercept(y, sample_weight, link, distribution, offset) - assert np.isfinite(best_intercept) - - def _get_dev(intercept): - eta = intercept if offset is None else offset + intercept - mu = link.inverse(eta) - assert np.isfinite(mu).all() - return distribution.deviance(y, mu, sample_weight) - - obj = _get_dev(best_intercept) - obj_low = _get_dev(best_intercept - tol) - obj_high = _get_dev(best_intercept + tol) - assert obj < obj_low - assert obj < obj_high - - @pytest.mark.parametrize("tol", [1e-2, 1e-4, 1e-6]) def test_step_size_tolerance(tol): - X, y = make_regression( + X, y = skl.datasets.make_regression( n_samples=100, n_features=5, noise=0.5, @@ -1643,8 +1029,8 @@ def build_glm(step_size_tol): baseline = build_glm(1e-10) glm = build_glm(tol) - assert_allclose(baseline.intercept_, glm.intercept_, atol=tol) - assert_allclose(baseline.coef_, glm.coef_, atol=tol) + np.testing.assert_allclose(baseline.intercept_, glm.intercept_, atol=tol) + np.testing.assert_allclose(baseline.coef_, glm.coef_, atol=tol) def test_alpha_search(regression_data): @@ -1669,8 +1055,8 @@ def test_alpha_search(regression_data): ) mdl_path.fit(X=X, y=y) - assert_allclose(mdl_path.coef_, mdl_no_path.coef_) - assert_allclose(mdl_path.intercept_, mdl_no_path.intercept_) + np.testing.assert_allclose(mdl_path.coef_, mdl_no_path.coef_) + np.testing.assert_allclose(mdl_path.intercept_, mdl_no_path.intercept_) @pytest.mark.parametrize("alpha, alpha_index", [(0.5, 0), (0.75, 1), (None, 1)]) @@ -1791,6 +1177,7 @@ def test_column_with_stddev_zero(): model = GeneralizedLinearRegressor( family="poisson", fit_intercept=False, scale_predictors=False ).fit(X, y) # noqa: F841 + model = GeneralizedLinearRegressor(family="poisson").fit(X, y) # noqa: F841 @@ -2757,9 +2144,9 @@ def test_information_criteria(regression_data): llf = regressor.family_instance.log_likelihood(y, regressor.predict(X)) nobs, df = X.shape[0], X.shape[1] + 1 - sm_aic = eval_measures.aic(llf, nobs, df) - sm_bic = eval_measures.bic(llf, nobs, df) - sm_aicc = eval_measures.aicc(llf, nobs, df) + sm_aic = statsmodels.tools.eval_measures.aic(llf, nobs, df) + sm_bic = statsmodels.tools.eval_measures.bic(llf, nobs, df) + sm_aicc = statsmodels.tools.eval_measures.aicc(llf, nobs, df) assert np.allclose( [sm_aic, sm_aicc, sm_bic], @@ -3015,313 +2402,6 @@ def test_store_covariance_matrix_cv( ) -@pytest.mark.parametrize( - "input, expected", - [ - pytest.param( - "y ~ x1 + x2", - (["y"], ["1", "x1", "x2"]), - id="implicit_intercept", - ), - pytest.param( - "y ~ x1 + x2 + 1", - (["y"], ["1", "x1", "x2"]), - id="explicit_intercept", - ), - pytest.param( - "y ~ x1 + x2 - 1", - (["y"], ["x1", "x2"]), - id="no_intercept", - ), - pytest.param( - "y ~ ", - (["y"], ["1"]), - id="empty_rhs", - ), - ], -) -def test_parse_formula(input, expected): - lhs_exp, rhs_exp = expected - lhs, rhs = _parse_formula(input) - assert list(lhs) == lhs_exp - assert list(rhs) == rhs_exp - - formula = Formula(input) - lhs, rhs = _parse_formula(formula) - assert list(lhs) == lhs_exp - assert list(rhs) == rhs_exp - - -@pytest.mark.parametrize( - "input, error", - [ - pytest.param("y1 + y2 ~ x1 + x2", ValueError, id="multiple_lhs"), - pytest.param([["y"], ["x1", "x2"]], TypeError, id="wrong_type"), - ], -) -def test_parse_formula_invalid(input, error): - with pytest.raises(error): - _parse_formula(input) - - -@pytest.fixture -def get_mixed_data(): - nrow = 10 - np.random.seed(0) - return pd.DataFrame( - { - "y": np.random.rand(nrow), - "x1": np.random.rand(nrow), - "x2": np.random.rand(nrow), - "c1": np.random.choice(["a", "b", "c"], nrow), - "c2": np.random.choice(["d", "e"], nrow), - } - ) - - -@pytest.mark.parametrize( - "formula", - [ - pytest.param("y ~ x1 + x2", id="numeric"), - pytest.param("y ~ c1", id="categorical"), - pytest.param("y ~ c1 * c2", id="categorical_interaction"), - pytest.param("y ~ x1 + x2 + c1 + c2", id="numeric_categorical"), - pytest.param("y ~ x1 * c1 * c2", id="numeric_categorical_interaction"), - ], -) -@pytest.mark.parametrize( - "drop_first", [True, False], ids=["drop_first", "no_drop_first"] -) -@pytest.mark.parametrize( - "fit_intercept", [True, False], ids=["intercept", "no_intercept"] -) -def test_formula(get_mixed_data, formula, drop_first, fit_intercept): - """Model with formula and model with externally constructed model matrix should - match. - """ - data = get_mixed_data - - model_formula = GeneralizedLinearRegressor( - family="normal", - drop_first=drop_first, - formula=formula, - fit_intercept=fit_intercept, - categorical_format="{name}[T.{category}]", - alpha=1.0, - ).fit(data) - - if fit_intercept: - # full rank check must consider presence of intercept - y_ext, X_ext = formulaic.model_matrix( - formula, - data, - ensure_full_rank=drop_first, - materializer=formulaic.materializers.PandasMaterializer, - ) - X_ext = X_ext.drop(columns="Intercept") - else: - y_ext, X_ext = formulaic.model_matrix( - formula + "-1", - data, - ensure_full_rank=drop_first, - materializer=formulaic.materializers.PandasMaterializer, - ) - y_ext = y_ext.iloc[:, 0] - - model_ext = GeneralizedLinearRegressor( - family="normal", - drop_first=drop_first, - fit_intercept=fit_intercept, - categorical_format="{name}[T.{category}]", - alpha=1.0, - ).fit(X_ext, y_ext) - - np.testing.assert_almost_equal(model_ext.coef_, model_formula.coef_) - - -def test_formula_explicit_intercept(get_mixed_data): - data = get_mixed_data - - with pytest.raises(ValueError, match="The formula sets the intercept to False"): - GeneralizedLinearRegressor( - family="normal", - formula="y ~ x1 - 1", - fit_intercept=True, - ).fit(data) - - -@pytest.mark.parametrize( - "formula, feature_names, term_names", - [ - pytest.param("y ~ x1 + x2", ["x1", "x2"], ["x1", "x2"], id="numeric"), - pytest.param( - "y ~ c1", ["c1[T.a]", "c1[T.b]", "c1[T.c]"], 3 * ["c1"], id="categorical" - ), - pytest.param( - "y ~ x1 : c1", - ["x1:c1[T.a]", "x1:c1[T.b]", "x1:c1[T.c]"], - 3 * ["x1:c1"], - id="interaction", - ), - pytest.param( - "y ~ poly(x1, 3)", - ["poly(x1, 3)[1]", "poly(x1, 3)[2]", "poly(x1, 3)[3]"], - 3 * ["poly(x1, 3)"], - id="function", - ), - ], -) -def test_formula_names_formulaic_style( - get_mixed_data, formula, feature_names, term_names -): - data = get_mixed_data - model_formula = GeneralizedLinearRegressor( - family="normal", - drop_first=False, - formula=formula, - categorical_format="{name}[T.{category}]", - interaction_separator=":", - alpha=1.0, - ).fit(data) - - np.testing.assert_array_equal(model_formula.feature_names_, feature_names) - np.testing.assert_array_equal(model_formula.term_names_, term_names) - - -@pytest.mark.parametrize( - "formula, feature_names, term_names", - [ - pytest.param("y ~ x1 + x2", ["x1", "x2"], ["x1", "x2"], id="numeric"), - pytest.param( - "y ~ c1", ["c1__a", "c1__b", "c1__c"], 3 * ["c1"], id="categorical" - ), - pytest.param( - "y ~ x1 : c1", - ["x1__x__c1__a", "x1__x__c1__b", "x1__x__c1__c"], - 3 * ["x1:c1"], - id="interaction", - ), - pytest.param( - "y ~ poly(x1, 3)", - ["poly(x1, 3)[1]", "poly(x1, 3)[2]", "poly(x1, 3)[3]"], - 3 * ["poly(x1, 3)"], - id="function", - ), - ], -) -def test_formula_names_old_glum_style( - get_mixed_data, formula, feature_names, term_names -): - data = get_mixed_data - model_formula = GeneralizedLinearRegressor( - family="normal", - drop_first=False, - formula=formula, - categorical_format="{name}__{category}", - interaction_separator="__x__", - alpha=1.0, - ).fit(data) - - np.testing.assert_array_equal(model_formula.feature_names_, feature_names) - np.testing.assert_array_equal(model_formula.term_names_, term_names) - - -@pytest.mark.parametrize( - "formula", - [ - pytest.param("y ~ x1 + x2", id="numeric"), - pytest.param("y ~ c1", id="categorical"), - pytest.param("y ~ c1 * c2", id="categorical_interaction"), - ], -) -@pytest.mark.parametrize( - "fit_intercept", [True, False], ids=["intercept", "no_intercept"] -) -def test_formula_against_smf(get_mixed_data, formula, fit_intercept): - data = get_mixed_data - model_formula = GeneralizedLinearRegressor( - family="normal", - drop_first=True, - formula=formula, - fit_intercept=fit_intercept, - ).fit(data) - - if fit_intercept: - beta_formula = np.concatenate([[model_formula.intercept_], model_formula.coef_]) - else: - beta_formula = model_formula.coef_ - - formula_smf = formula + "- 1" if not fit_intercept else formula - model_smf = smf.glm(formula_smf, data, family=sm.families.Gaussian()).fit() - - np.testing.assert_almost_equal(beta_formula, model_smf.params) - - -def test_formula_context(get_mixed_data): - data = get_mixed_data - x_context = np.arange(len(data), dtype=float) # noqa: F841 - formula = "y ~ x1 + x2 + x_context" - - model_formula = GeneralizedLinearRegressor( - family="normal", - drop_first=True, - formula=formula, - fit_intercept=True, - ) - # default is to add nothing to context - with pytest.raises(formulaic.errors.FactorEvaluationError): - model_formula.fit(data) - - # set context to 0 to capture calling scope - model_formula = GeneralizedLinearRegressor( - family="normal", - drop_first=True, - formula=formula, - fit_intercept=True, - ).fit(data, context=0) - - model_smf = smf.glm(formula, data, family=sm.families.Gaussian()).fit() - - np.testing.assert_almost_equal( - np.concatenate([[model_formula.intercept_], model_formula.coef_]), - model_smf.params, - ) - np.testing.assert_almost_equal( - model_formula.predict(data, context=0), model_smf.predict(data) - ) - - -@pytest.mark.parametrize( - "formula", - [ - pytest.param("y ~ x1 + x2", id="numeric"), - pytest.param("y ~ c1", id="categorical"), - pytest.param("y ~ c1 * c2", id="categorical_interaction"), - ], -) -@pytest.mark.parametrize( - "fit_intercept", [True, False], ids=["intercept", "no_intercept"] -) -def test_formula_predict(get_mixed_data, formula, fit_intercept): - data = get_mixed_data - data_unseen = data.copy() - data_unseen.loc[data_unseen["c1"] == "b", "c1"] = "c" - model_formula = GeneralizedLinearRegressor( - family="normal", - drop_first=True, - formula=formula, - fit_intercept=fit_intercept, - ).fit(data) - - formula_smf = formula + "- 1" if not fit_intercept else formula - model_smf = smf.glm(formula_smf, data, family=sm.families.Gaussian()).fit() - - yhat_formula = model_formula.predict(data_unseen) - yhat_smf = model_smf.predict(data_unseen) - - np.testing.assert_almost_equal(yhat_formula, yhat_smf) - - @pytest.mark.parametrize("cat_missing_method", ["fail", "zero", "convert"]) @pytest.mark.parametrize("unseen_missing", [False, True]) @pytest.mark.parametrize("formula", [None, "cat_1 + cat_2"]) diff --git a/tests/glm/test_golden_master.py b/tests/glm/test_golden_master.py index 4f70c6a7e..cbf54a531 100644 --- a/tests/glm/test_golden_master.py +++ b/tests/glm/test_golden_master.py @@ -9,8 +9,9 @@ from git_root import git_root from scipy import sparse -from glum import GeneralizedLinearRegressor, GeneralizedLinearRegressorCV -from glum._glm import TweedieDistribution +from glum._distribution import TweedieDistribution +from glum._glm_cv import GeneralizedLinearRegressorCV +from glum._glm_regressor import GeneralizedLinearRegressor from glum_benchmarks.data import simulate_glm_data distributions_to_test = ["normal", "poisson", "gamma", "tweedie_p=1.5", "binomial"] diff --git a/tests/glm/test_intercept.py b/tests/glm/test_intercept.py new file mode 100644 index 000000000..3782f8b53 --- /dev/null +++ b/tests/glm/test_intercept.py @@ -0,0 +1,56 @@ +import numpy as np +import pytest + +from glum._distribution import ( + BinomialDistribution, + ExponentialDispersionModel, + GammaDistribution, + GeneralizedHyperbolicSecant, + InverseGaussianDistribution, + NegativeBinomialDistribution, + NormalDistribution, + PoissonDistribution, + TweedieDistribution, + guess_intercept, +) +from glum._link import IdentityLink, Link, LogitLink, LogLink + + +@pytest.mark.parametrize( + "link, distribution, tol", + [ + (IdentityLink(), NormalDistribution(), 1e-4), + (LogLink(), PoissonDistribution(), 1e-4), + (LogLink(), GammaDistribution(), 1e-4), + (LogLink(), TweedieDistribution(1.5), 1e-4), + (LogLink(), TweedieDistribution(4.5), 1e-4), + (LogLink(), NormalDistribution(), 1e-4), + (LogLink(), InverseGaussianDistribution(), 1e-4), + (LogLink(), NegativeBinomialDistribution(), 1e-2), + (LogitLink(), BinomialDistribution(), 1e-2), + (IdentityLink(), GeneralizedHyperbolicSecant(), 1e-1), + ], +) +@pytest.mark.parametrize("offset", [None, np.array([0.3, -0.1, 0, 0.1]), 0.1]) +def test_get_best_intercept( + link: Link, distribution: ExponentialDispersionModel, tol: float, offset +): + y = np.array([1, 1, 1, 2], dtype=np.float64) + if isinstance(distribution, BinomialDistribution): + y -= 1 + + sample_weight = np.array([0.1, 0.2, 5, 1]) + best_intercept = guess_intercept(y, sample_weight, link, distribution, offset) + assert np.isfinite(best_intercept) + + def _get_dev(intercept): + eta = intercept if offset is None else offset + intercept + mu = link.inverse(eta) + assert np.isfinite(mu).all() + return distribution.deviance(y, mu, sample_weight) + + obj = _get_dev(best_intercept) + obj_low = _get_dev(best_intercept - tol) + obj_high = _get_dev(best_intercept + tol) + assert obj < obj_low + assert obj < obj_high diff --git a/tests/glm/test_offset_weight_obj_equivalent.py b/tests/glm/test_offset_weight_obj_equivalent.py index 525e76552..791a82792 100644 --- a/tests/glm/test_offset_weight_obj_equivalent.py +++ b/tests/glm/test_offset_weight_obj_equivalent.py @@ -15,6 +15,7 @@ for k, v in get_all_problems().items() if "offset" in k and "gaussian" not in k and "binomial" not in k } + bench_cfg = dict( num_rows=10000, regularization_strength=0.1, diff --git a/tests/glm/test_utils.py b/tests/glm/test_utils.py index 614717502..7bc58ee2e 100644 --- a/tests/glm/test_utils.py +++ b/tests/glm/test_utils.py @@ -2,7 +2,7 @@ import pandas as pd import pytest -from glum._util import _add_missing_categories, _align_df_categories +from glum._utils import add_missing_categories, align_df_categories @pytest.fixture() @@ -40,7 +40,7 @@ def test_align_df_categories_numeric(df): ) pd.testing.assert_frame_equal( - _align_df_categories(df, dtypes, has_missing_category, missing_method), expected + align_df_categories(df, dtypes, has_missing_category, missing_method), expected ) @@ -61,7 +61,7 @@ def test_align_df_categories_categorical(df): ) pd.testing.assert_frame_equal( - _align_df_categories(df, dtypes, has_missing_category, missing_method), + align_df_categories(df, dtypes, has_missing_category, missing_method), expected, ) @@ -85,7 +85,7 @@ def test_align_df_categories_excess_columns(df): ) pd.testing.assert_frame_equal( - _align_df_categories(df, dtypes, has_missing_category, missing_method), expected + align_df_categories(df, dtypes, has_missing_category, missing_method), expected ) @@ -108,7 +108,7 @@ def test_align_df_categories_missing_columns(df): ) pd.testing.assert_frame_equal( - _align_df_categories(df, dtypes, has_missing_category, missing_method), expected + align_df_categories(df, dtypes, has_missing_category, missing_method), expected ) @@ -131,7 +131,7 @@ def test_align_df_categories_convert(df, has_missings): if has_missings: pd.testing.assert_frame_equal( - _align_df_categories( + align_df_categories( df[["x5", "x6", "x7", "x8"]], dtypes, has_missing_category, @@ -141,7 +141,7 @@ def test_align_df_categories_convert(df, has_missings): ) else: with pytest.raises(ValueError, match="contains unseen categories"): - _align_df_categories( + align_df_categories( df[["x5", "x6", "x7", "x8"]], dtypes, has_missing_category, @@ -155,7 +155,7 @@ def test_align_df_categories_raise_on_unseen(df): missing_method = "fail" with pytest.raises(ValueError, match="contains unseen categories"): - _align_df_categories( + align_df_categories( df, dtypes, has_missing_category, @@ -165,7 +165,7 @@ def test_align_df_categories_raise_on_unseen(df): def test_align_df_categories_not_df(): with pytest.raises(TypeError): - _align_df_categories(np.array([[0], [1]]), {"x0": np.float64}, {}, "fail") + align_df_categories(np.array([[0], [1]]), {"x0": np.float64}, {}, "fail") @pytest.fixture() @@ -206,7 +206,7 @@ def test_add_missing_categories(df_na): ) pd.testing.assert_frame_equal( - _add_missing_categories( + add_missing_categories( df=df_na, dtypes=dtypes, feature_names=feature_names, @@ -238,7 +238,7 @@ def test_raise_on_existing_missing(df_na): df.loc[df.cat_na.isna(), "cat_na"] = "(M)" with pytest.raises(ValueError): - _add_missing_categories( + add_missing_categories( df=df, dtypes=dtypes, feature_names=feature_names,