diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index d271c6e..4318330 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 @@ -45,4 +45,4 @@ jobs: - name: Test with pytest run: | - coverage run -m pytest tests && coverage report -m --fail-under=75 + coverage run -m pytest tests && coverage report -m --fail-under=50 diff --git a/.gitignore b/.gitignore index d828387..0134fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ build/ *.egg-info/ .coverage +.tox # Documentation artifacts _build/ diff --git a/error_parity/_version.py b/error_parity/_version.py index 783a634..99ab443 100644 --- a/error_parity/_version.py +++ b/error_parity/_version.py @@ -1,3 +1,3 @@ """File to keep the package version in one place.""" -__version__ = "0.3.11" +__version__ = "0.3.12" __version_info__ = tuple(__version__.split(".")) diff --git a/error_parity/cvxpy_utils.py b/error_parity/cvxpy_utils.py index 364abe9..fd1e41c 100644 --- a/error_parity/cvxpy_utils.py +++ b/error_parity/cvxpy_utils.py @@ -416,6 +416,14 @@ def group_positive_prediction_rate(group_idx: int): # Run solver prob.solve(solver=cp.ECOS, abstol=SOLUTION_TOLERANCE, feastol=SOLUTION_TOLERANCE) + + # NOTE: ECOS solver has been deprecated in favor of CLARABEL in cvxpy 1.3.2+ + # https://www.cvxpy.org/updates/index.html?h=ecos#ecos-deprecation + # prob.solve( + # solver=cp.CLARABEL, + # tol_gap_abs=SOLUTION_TOLERANCE, + # tol_feas=SOLUTION_TOLERANCE, + # ) # NOTE: these tolerances are supposed to be smaller than the default np.isclose tolerances # (useful when comparing if two points are the same, within the cvxpy accuracy tolerance) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b5ee68c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,152 @@ +[build-system] +requires = ["setuptools>=77.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "error-parity" +description = "Achieve error-rate parity between protected groups for any predictor" +license = "MIT" +license-files = ["LICENSE"] +authors = [ + { name = "AndreFCruz" }, +] + +# Keywords to be used by PyPI search +keywords = ["ml", "optimization", "fairness", "error-parity", "equal-odds"] + +# PyPI classifiers, see https://pypi.org/classifiers/ +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +requires-python = ">=3.9" + +# These are defined below dynamically: +dynamic = [ + "version", + "readme", + "dependencies", + "optional-dependencies", +] + + +[tool.setuptools.packages.find] +include = ["error_parity*"] +exclude = ["tests*"] + +[tool.setuptools.dynamic] +version = { attr = "error_parity._version.__version__" } +readme = { file = "README.md", content-type = "text/markdown" } + +# Main package dependencies +dependencies = {file = "requirements/main.txt"} + +# Optional dependencies +[tool.setuptools.dynamic.optional-dependencies] +test = {file = "requirements/test.txt"} +dev = {file = "requirements/dev.txt"} +docs = {file = "requirements/docs.txt"} +all = {file = [ + "requirements/dev.txt", + "requirements/test.txt", + "requirements/docs.txt", +]} + +[project.urls] +homepage = "https://github.com/socialfoundations/error-parity" + +# flake8 +[tool.flake8] +max-complexity = 10 +max-line-length = 120 + +per-file-ignores = """ +# imported but unused +**/__init__.py: F401 +""" + +exclude = [ + "docs/", + ".tox/", + "build/", + "dist/", +] + +[tool.pytest.ini_options] +minversion = "8.0" +testpaths = [ + "tests", +] + +# isort +[tool.isort] +profile = "hug" +force_single_line = false +src_paths = ["error_parity", "tests"] + +# Coverage +[tool.coverage.run] +branch = true +source = ["error_parity"] +omit = ["error_parity/_version.py", "tests"] + +[tool.coverage.report] +show_missing = true + +# MyPy +[tool.mypy] +ignore_missing_imports = true +no_implicit_optional = false +strict_optional = false +exclude = [ + "build", + "doc", + "tests", + "notebooks", +] +python_version = "3.11" + +# Tox +[tool.tox] +legacy_tox_ini = """ +[tox] +env_list = + py39 + py310 + py311 + py312 + lint + type + +[testenv] +description = run unit tests +deps = + pytest>=8 + coverage>=7 +commands = + coverage erase + coverage run -m pytest {posargs:tests} + coverage report -m + +[testenv:type] +description = run type checks +basepython = python3.11 +deps = + mypy>=1.0 +commands = mypy {posargs:error_parity} + +[testenv:lint] +description = run linters +skip_install = true +deps = + flake8>=7.0 + flake8-pyproject +commands = flake8 {posargs:error_parity tests} +""" diff --git a/requirements/main.txt b/requirements/main.txt index 9ff393f..26d3aa0 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -1,8 +1,8 @@ pandas -numpy +numpy<2.0.0 scipy tqdm scikit-learn>=1.2 -cvxpy>=1.3.2 +cvxpy[ecos,clarabel]~=1.3.2 matplotlib seaborn diff --git a/requirements/test.txt b/requirements/test.txt index 429791f..89bd7bb 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,5 +1,5 @@ -pytest -coverage +pytest>=8.0 +coverage>=7.0 folktables tqdm mypy diff --git a/setup.py b/setup.py deleted file mode 100644 index 01e9bfa..0000000 --- a/setup.py +++ /dev/null @@ -1,97 +0,0 @@ -"""This module contains utils to setup a standalone installable package.""" - -import re -from pathlib import Path -from setuptools import setup, find_packages - - -def stream_requirements(fd): - """For a given requirements file descriptor, generate lines of - distribution requirements, ignoring comments and chained requirement - files. - """ - for line in fd: - cleaned = re.sub(r"#.*$", "", line).strip() - if cleaned and not cleaned.startswith("-r"): - yield cleaned - - -def load_requirements(txt_path): - """Short helper for loading requirements from a .txt file. - - Parameters - ---------- - txt_path : Path or str - Path to the requirements file. - - Returns - ------- - list - List of requirements, one list element per line in the text file. - """ - with Path(txt_path).open() as requirements_file: - return list(stream_requirements(requirements_file)) - - -# ---------------------------------------------------------------------------- # -# Requirements # -# ---------------------------------------------------------------------------- # - -ROOT_PATH = Path(__file__).parent -README_PATH = ROOT_PATH / "README.md" - -REQUIREMENTS_PATH = ROOT_PATH / "requirements" / "main.txt" -requirements = load_requirements(REQUIREMENTS_PATH) - -DEV_REQUIREMENTS_PATH = ROOT_PATH / "requirements" / "dev.txt" -dev_requirements = load_requirements(DEV_REQUIREMENTS_PATH) - -TEST_REQUIREMENTS_PATH = ROOT_PATH / "requirements" / "test.txt" -test_requirements = load_requirements(TEST_REQUIREMENTS_PATH) - - -# ---------------------------------------------------------------------------- # -# Version # -# ---------------------------------------------------------------------------- # -SRC_PATH = ROOT_PATH / "error_parity" -VERSION_PATH = SRC_PATH / "_version.py" - -with VERSION_PATH.open("rb") as version_file: - exec(version_file.read()) - - -# ---------------------------------------------------------------------------- # -# SETUP # -# ---------------------------------------------------------------------------- # -setup( - name="error-parity", - version=__version__, - description="Achieve error-rate parity between protected groups for any predictor", - keywords=["ml", "optimization", "fairness", "error-parity", "equal-odds"], - long_description=(README_PATH).read_text(), - long_description_content_type="text/markdown", - python_requires=">=3.8", - packages=find_packages(), - install_requires=requirements, - extras_require={ - "test": test_requirements, - "dev": dev_requirements, - "all": dev_requirements + test_requirements, - }, - author="AndreFCruz", - url="https://github.com/socialfoundations/error-parity", - license="MIT", - classifiers=[ - "Development Status :: 3 - Alpha", - "License :: OSI Approved :: MIT License", - "Intended Audience :: Science/Research", - "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Natural Language :: English", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], -) diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py index d45c33a..9bab4c8 100644 --- a/tests/test_evaluation.py +++ b/tests/test_evaluation.py @@ -68,5 +68,5 @@ def test_equalized_odds_relaxation_costs( higher_p_cost = results[higher_p_norm] # Assert lower-p costs are higher (accuracy is lower) - assert lower_p_cost > higher_p_cost - SOLUTION_TOLERANCE, \ + assert lower_p_cost > higher_p_cost - 2 * SOLUTION_TOLERANCE, \ f"l-{lower_p_norm} cost: {lower_p_cost} < l-{higher_p_norm} cost: {higher_p_cost}"