From bfad4c912bdaff1f861eb70f4c9c0417ce7ed9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Perceval=20Wajsb=C3=BCrt?= Date: Thu, 7 Dec 2023 09:18:29 +0100 Subject: [PATCH 1/5] fix: patch pyarrow.open_stream to support pyarrow>0.17 --- changelog.md | 6 +++ docs/generate_reference.py | 2 +- eds_scikit/__init__.py | 8 +++- .../package-override/pyarrow/__init__.py | 38 +++++++++++++++++++ pyproject.toml | 2 +- 5 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 eds_scikit/package-override/pyarrow/__init__.py diff --git a/changelog.md b/changelog.md index fd08949a..831998de 100644 --- a/changelog.md +++ b/changelog.md @@ -1,10 +1,16 @@ # Changelog ## Unreleased + +### Changed + +- Support for pyarrow > 0.17.0 + ### Fixed - Caching in spark instead of koalas to improve speed ## v0.1.6 (2023-09-27) + ### Added - Module ``event_sequences`` to visualize individual sequences of events. - Module ``age_pyramid`` to quickly visualize the age and gender distributions in a cohort. diff --git a/docs/generate_reference.py b/docs/generate_reference.py index 2be7cf8f..b3863353 100644 --- a/docs/generate_reference.py +++ b/docs/generate_reference.py @@ -8,7 +8,7 @@ for path in sorted(Path("eds_scikit").rglob("*.py")): print(path) - if ".ipynb_checkpoints" in path.parts: + if ".ipynb_checkpoints" in path.parts or "package-override" in path.parts: continue module_path = path.relative_to(".").with_suffix("") doc_path = path.relative_to("eds_scikit").with_suffix(".md") diff --git a/eds_scikit/__init__.py b/eds_scikit/__init__.py index 52cc3140..96e0583f 100644 --- a/eds_scikit/__init__.py +++ b/eds_scikit/__init__.py @@ -11,6 +11,7 @@ import importlib import os +import pathlib import sys import time from packaging import version @@ -19,6 +20,7 @@ import pandas as pd import pyarrow +import pyarrow.ipc import pyspark from loguru import logger from pyspark import SparkContext @@ -26,8 +28,12 @@ import eds_scikit.biology # noqa: F401 --> To register functions -import eds_scikit.utils.logging +pyarrow.open_stream = pyarrow.ipc.open_stream +sys.path.insert( + 0, (pathlib.Path(__file__).parent / "package-override").absolute().as_posix() +) +os.environ["PYTHONPATH"] = ":".join(sys.path) # Remove SettingWithCopyWarning pd.options.mode.chained_assignment = None diff --git a/eds_scikit/package-override/pyarrow/__init__.py b/eds_scikit/package-override/pyarrow/__init__.py new file mode 100644 index 00000000..a64b54d9 --- /dev/null +++ b/eds_scikit/package-override/pyarrow/__init__.py @@ -0,0 +1,38 @@ +""" +PySpark 2 needs pyarrow.open_stream, which was deprecated in 0.17.0 in favor of +pyarrow.ipc.open_stream. Here is the explanation of how we monkey-patch pyarrow +to add back pyarrow.open_stream for versions > 0.17 and how we make this work with +pyspark distributed computing : +1. We add this fake eds_scikit/package-override/pyarrow package to python lookup list + (the PYTHONPATH env var) in eds_scikit/__init__.py : this env variable will be shared + with the executors +2. When an executor starts and import packages, it looks in the packages by inspecting + the paths in PYTHONPATH. It finds our fake pyarrow package first an executes the + current eds_scikit/package-override/pyarrow/__init__.py file +3. In this file, we remove the fake pyarrow package path from the lookup list, unload + the current module from python modules cache (sys.modules) and re-import pyarrow + => the executor's python will this time load the true pyarrow and store it in + sys.modules. Subsequent "import pyarrow" calls will return the sys.modules["pyarrow"] + value, which is the true pyarrow module. +4. We are not finished: we add back the deprecated "open_stream" function that was + removed in pyarrow 0.17.0 (the reason for all this hacking) by setting it + on the true pyarrow module +5. We still export the pyarrow module content (*) such that the first import, which + is the only one that resolves to this very module, still gets what it asked for: + the pyarrow module's content. +""" + +import sys + +sys.path.remove(next((p for p in sys.path if "package-override" in p), None)) +del sys.modules["pyarrow"] +import pyarrow # noqa: E402, F401 + +try: + import pyarrow.ipc + + pyarrow.open_stream = pyarrow.ipc.open_stream +except ImportError: + pass + +from pyarrow import * # noqa: F401, F403, E402 diff --git a/pyproject.toml b/pyproject.toml index 0a5310c2..54c81f03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ "loguru==0.7.0", "pypandoc==1.7.5", "pyspark==2.4.3", - "pyarrow==0.17.0", #"pyarrow>=0.10, <0.17.0", + "pyarrow>=0.10.0", "pretty-html-table>=0.9.15, <0.10.0", "catalogue", "schemdraw>=0.15.0, <1.0.0", From 31ba5f20bd5ac49523e96eced1ec00f1474bd6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Perceval=20Wajsb=C3=BCrt?= Date: Thu, 7 Dec 2023 10:25:17 +0100 Subject: [PATCH 2/5] test: import utils.logging to improve coverage --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index ca57ef17..f90caa04 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ from databricks import koalas as ks from loguru import logger +import eds_scikit.utils.logging # noqa: F401 from eds_scikit import improve_performances from . import test_registry # noqa: F401 --> To register functions From a13fc93fcf95f6880bd4590c35c9597063914c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Perceval=20Wajsb=C3=BCrt?= Date: Thu, 7 Dec 2023 19:29:45 +0100 Subject: [PATCH 3/5] feat: support pyspark 3 (via a databricks.koalas stub) --- .github/workflows/testing.yml | 30 ++++++++++++++----- build_tools/github/install.sh | 4 --- build_tools/github/test.sh | 4 --- changelog.md | 4 ++- docs/project_description.md | 1 + eds_scikit/__init__.py | 4 +-- .../package-override/databricks/__init__.py | 0 .../databricks/koalas/__init__.py | 17 +++++++++++ .../package-override/pyarrow/__init__.py | 13 ++++---- pyproject.toml | 15 ++++++---- tests/conftest.py | 8 ++--- tests/test_biology.py | 6 +++- tests/test_convert.py | 4 +-- 13 files changed, 71 insertions(+), 39 deletions(-) delete mode 100755 build_tools/github/install.sh delete mode 100755 build_tools/github/test.sh create mode 100644 eds_scikit/package-override/databricks/__init__.py create mode 100644 eds_scikit/package-override/databricks/koalas/__init__.py diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a1e13b41..55430594 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -49,6 +49,20 @@ jobs: needs: check_skip if: ${{ needs.check_skip.outputs.skip == 'false' }} runs-on: "ubuntu-latest" + strategy: + fail-fast: true + matrix: + include: + - python-version: "3.7" + spark: "spark2" + - python-version: "3.7" + spark: "spark3" + - python-version: "3.8" + spark: "spark3" + - python-version: "3.9" + spark: "spark3" + - python-version: "3.10" + spark: "spark3" name: 'Testing on ubuntu' defaults: run: @@ -61,13 +75,15 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.7' - - name: Install eds-scikit - shell: bash {0} - run: ./build_tools/github/install.sh - - name: Run tests - shell: bash {0} - run: ./build_tools/github/test.sh + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + pip install -U "pip<23" + pip install --progress-bar off ".[${{ matrix.spark }}, dev, doc]" + - name: Run pytest + run: | + python -m pytest --pyargs tests -m "" --cov=eds_scikit - name: Upload coverage to CodeCov uses: codecov/codecov-action@v3 if: success() diff --git a/build_tools/github/install.sh b/build_tools/github/install.sh deleted file mode 100755 index 4612b415..00000000 --- a/build_tools/github/install.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -e - -pip install -U "pip<23" -pip install --progress-bar off --upgrade ".[dev, doc]" diff --git a/build_tools/github/test.sh b/build_tools/github/test.sh deleted file mode 100755 index f28f9358..00000000 --- a/build_tools/github/test.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -x - -pip install -U "pip<23" -python -m pytest --pyargs tests -m "" --cov=eds_scikit diff --git a/changelog.md b/changelog.md index 831998de..b6748545 100644 --- a/changelog.md +++ b/changelog.md @@ -2,9 +2,11 @@ ## Unreleased -### Changed +### Added - Support for pyarrow > 0.17.0 +- Support for Python 3.7 to 3.10 (3.11 or higher is not tested) +- Support for pyspark 3 (to force pyspark 2, use `pip install eds-scikit[spark2]`) ### Fixed - Caching in spark instead of koalas to improve speed diff --git a/docs/project_description.md b/docs/project_description.md index 97fb139f..1d1321b7 100644 --- a/docs/project_description.md +++ b/docs/project_description.md @@ -124,6 +124,7 @@ The goal of **Koalas** is precisely to avoid this issue. It aims at allowing cod ```python from databricks import koalas as ks +# or from pyspark import pandas as ks, if you have spark 3 # Converting the Spark DataFrame into a Koalas DataFrame visit_occurrence_koalas = visit_occurrence_spark.to_koalas() diff --git a/eds_scikit/__init__.py b/eds_scikit/__init__.py index 96e0583f..72070e42 100644 --- a/eds_scikit/__init__.py +++ b/eds_scikit/__init__.py @@ -26,8 +26,6 @@ from pyspark import SparkContext from pyspark.sql import SparkSession -import eds_scikit.biology # noqa: F401 --> To register functions - pyarrow.open_stream = pyarrow.ipc.open_stream sys.path.insert( @@ -35,6 +33,8 @@ ) os.environ["PYTHONPATH"] = ":".join(sys.path) +import eds_scikit.biology # noqa: F401 --> To register functions + # Remove SettingWithCopyWarning pd.options.mode.chained_assignment = None diff --git a/eds_scikit/package-override/databricks/__init__.py b/eds_scikit/package-override/databricks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/eds_scikit/package-override/databricks/koalas/__init__.py b/eds_scikit/package-override/databricks/koalas/__init__.py new file mode 100644 index 00000000..b99906b4 --- /dev/null +++ b/eds_scikit/package-override/databricks/koalas/__init__.py @@ -0,0 +1,17 @@ +# This file is used to override the databricks.koalas package with the pyspark.pandas +# package, if the databricks.koalas package is not available (python >= 3.8) +import sys +import pyarrow # noqa: E402, F401 + +old_sys_path = sys.path.copy() +sys.path.remove(next((p for p in sys.path if "package-override" in p), None)) +databricks = sys.modules.pop("databricks") +sys.modules.pop("databricks.koalas") +try: + from databricks.koalas import * # noqa: E402, F401, F403 +except ImportError: + from pyspark.pandas import * # noqa: E402, F401, F403 + + sys.modules["databricks"] = databricks + sys.modules["databricks.koalas"] = sys.modules["pyspark.pandas"] +sys.path[:] = old_sys_path diff --git a/eds_scikit/package-override/pyarrow/__init__.py b/eds_scikit/package-override/pyarrow/__init__.py index a64b54d9..c2200a55 100644 --- a/eds_scikit/package-override/pyarrow/__init__.py +++ b/eds_scikit/package-override/pyarrow/__init__.py @@ -21,18 +21,17 @@ is the only one that resolves to this very module, still gets what it asked for: the pyarrow module's content. """ - import sys +old_sys_path = sys.path.copy() sys.path.remove(next((p for p in sys.path if "package-override" in p), None)) del sys.modules["pyarrow"] -import pyarrow # noqa: E402, F401 -try: - import pyarrow.ipc +import pyarrow # noqa: E402, F401 +from pyarrow.ipc import open_stream # noqa: E402, F401 - pyarrow.open_stream = pyarrow.ipc.open_stream -except ImportError: - pass +pyarrow.open_stream = open_stream from pyarrow import * # noqa: F401, F403, E402 + +sys.path[:] = old_sys_path diff --git a/pyproject.toml b/pyproject.toml index 54c81f03..b643f4af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,19 +35,18 @@ dependencies = [ "pgpasslib>=1.1.0, <2.0.0", "psycopg2-binary>=2.9.0, <3.0.0", "pandas>=1.3.0, <2.0.0", - "numpy>=1.0.0, <1.20", - "koalas>=1.8.1, <2.0.0", + "numpy>=1.0.0", "altair>=5.0.0, <6.0.0", "loguru==0.7.0", "pypandoc==1.7.5", - "pyspark==2.4.3", + "pyspark", "pyarrow>=0.10.0", "pretty-html-table>=0.9.15, <0.10.0", "catalogue", "schemdraw>=0.15.0, <1.0.0", - "ipython>=7.32.0, <8.0.0", - "packaging==21.3", - "tomli==2.0.1", + "ipython>=7.32.0", + "packaging>=21.3", + "tomli>=2.0.1", ] dynamic = ['version'] @@ -66,6 +65,10 @@ Documentation = "https://aphp.github.io/eds-scikit" "Bug Tracker" = "https://github.com/aphp/eds-scikit/issues" [project.optional-dependencies] +spark2 = [ + "pyspark==2.4.3", + "koalas>=1.8.1,<2.0.0", +] dev = [ "black>=22.3.0, <23.0.0", "flake8==3.9.2", diff --git a/tests/conftest.py b/tests/conftest.py index f90caa04..4c3ba10f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,8 @@ +# isort: skip_file import logging import os +import eds_scikit import pandas as pd import pytest from _pytest.logging import caplog as _caplog # noqa F401 @@ -84,11 +86,6 @@ def spark_session(pytestconfig, tmpdir_factory): SparkConf() .setMaster("local") .setAppName("testing") - # used to overwrite hive tables - .set( - "spark.sql.legacy.allowCreatingManagedTableUsingNonemptyLocation", - "true", - ) # Path to data and metastore # Note: the option "hive.metastore.warehouse.dir" is deprecated # But javax.jdo.option.ConnectionURL can be used for the path of 'metastrore_db' @@ -101,6 +98,7 @@ def spark_session(pytestconfig, tmpdir_factory): "javax.jdo.option.ConnectionURL", f"jdbc:derby:;databaseName={temp_warehouse_dir}/metastore_db;create=true", ) + .set("spark.executor.cores", 1) ) session, _, _ = improve_performances(to_add_conf=list(conf.getAll())) diff --git a/tests/test_biology.py b/tests/test_biology.py index eb046ec9..12af6e48 100644 --- a/tests/test_biology.py +++ b/tests/test_biology.py @@ -14,7 +14,10 @@ def tmp_biology_dir(tmp_path_factory): @pytest.fixture def data(): - return load_biology_data(seed=42) + return load_biology_data( + seed=42, + mean_measurement=500, + ) @pytest.fixture @@ -73,6 +76,7 @@ def test_biology_summary(data, concepts_sets, module, tmp_biology_dir): limit_count=("AnaBio", 500), stats_only=True, save_folder_path=tmp_biology_dir, + pd_limit_size=0, ) diff --git a/tests/test_convert.py b/tests/test_convert.py index 671bbe28..ee9b1f3d 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -51,9 +51,9 @@ def test_framework_koalas(example_objects): def test_unconvertible_objects(): objects = [1, "coucou", {"a": [1, 2]}, [1, 2, 3], 2.5, ks, pd] for obj in objects: - with pytest.raises(ValueError): + with pytest.raises((ValueError, TypeError)): framework.pandas(obj) for obj in objects: - with pytest.raises(ValueError): + with pytest.raises((ValueError, TypeError)): framework.koalas(obj) From 59d4c0656861d182118b9ebb13079cdf62c3cbcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Perceval=20Wajsb=C3=BCrt?= Date: Fri, 8 Dec 2023 11:07:53 +0100 Subject: [PATCH 4/5] test: allow for another output in test_flowchart --- eds_scikit/biology/utils/config.py | 2 +- eds_scikit/utils/test_utils.py | 19 +++++++++---------- tests/flowchart/expected_flowchart_bis.png | Bin 0 -> 20808 bytes tests/flowchart/test_flowchart.py | 11 ++++++----- 4 files changed, 16 insertions(+), 16 deletions(-) create mode 100644 tests/flowchart/expected_flowchart_bis.png diff --git a/eds_scikit/biology/utils/config.py b/eds_scikit/biology/utils/config.py index ede16420..ec29e0de 100644 --- a/eds_scikit/biology/utils/config.py +++ b/eds_scikit/biology/utils/config.py @@ -1,9 +1,9 @@ import glob +import os from pathlib import Path from typing import List import pandas as pd -from importlib_metadata import os from loguru import logger from eds_scikit.biology.utils.process_concepts import ConceptsSet diff --git a/eds_scikit/utils/test_utils.py b/eds_scikit/utils/test_utils.py index c0403e8a..53520964 100644 --- a/eds_scikit/utils/test_utils.py +++ b/eds_scikit/utils/test_utils.py @@ -96,7 +96,7 @@ def assert_equal( return output -def assert_images_equal(image_1: str, image_2: str): +def image_diff(image_1: str, image_2: str): img1 = Image.open(image_1) img2 = Image.open(image_2) @@ -105,12 +105,11 @@ def assert_images_equal(image_1: str, image_2: str): img2 = img2.resize(img1.size) sum_sq_diff = np.sum( - (np.asarray(img1).astype("float") - np.asarray(img2).astype("float")) ** 2 - ) - - if sum_sq_diff == 0: - # Images are exactly the same - pass - else: - normalized_sum_sq_diff = sum_sq_diff / np.sqrt(sum_sq_diff) - assert normalized_sum_sq_diff < 0.001 + ( + np.asarray(img1).astype("float") / 255 + - np.asarray(img2).astype("float") / 255 + ) + ** 2 + ) / np.prod(img1.size) + + return sum_sq_diff diff --git a/tests/flowchart/expected_flowchart_bis.png b/tests/flowchart/expected_flowchart_bis.png new file mode 100644 index 0000000000000000000000000000000000000000..3d6c42e259586b9f4a9c2976efd855bcc5ef934d GIT binary patch literal 20808 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 In(2lA0ksQw!vFvP literal 0 HcmV?d00001 diff --git a/tests/flowchart/test_flowchart.py b/tests/flowchart/test_flowchart.py index f127e4f3..bc0f98d1 100644 --- a/tests/flowchart/test_flowchart.py +++ b/tests/flowchart/test_flowchart.py @@ -5,7 +5,7 @@ import pytest from eds_scikit.utils.flowchart import Flowchart -from eds_scikit.utils.test_utils import assert_images_equal +from eds_scikit.utils.test_utils import image_diff data_as_df = pd.DataFrame( dict( @@ -63,12 +63,13 @@ def test_flowchart(data, tmpdir_factory): _ = F.generate_flowchart(alternate=True, fontsize=10) - result_path = tmp_dir / "flowchart.png" - F.save(result_path, dpi=72) + out_path = str(tmp_dir / "flowchart.png") + F.save(out_path, dpi=72) - expected = Path(__file__).parent / "expected_flowchart.png" + tgt_1 = str(Path(__file__).parent / "expected_flowchart.png") + tgt_2 = str(Path(__file__).parent / "expected_flowchart_bis.png") - assert_images_equal(result_path, expected) + assert image_diff(out_path, tgt_1) < 0.05 or image_diff(out_path, tgt_2) < 0.05 def test_incorrect_data(): From 2ce5aabe658d6236759bbdaaa78e6e38bddce004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Perceval=20Wajsb=C3=BCrt?= Date: Thu, 7 Dec 2023 21:27:11 +0100 Subject: [PATCH 5/5] fix: use new pip, bypass pypandoc w/ wheel & loosen constraints --- .github/workflows/testing.yml | 2 +- pyproject.toml | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 55430594..859e6887 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -79,7 +79,7 @@ jobs: cache: 'pip' - name: Install dependencies run: | - pip install -U "pip<23" + pip install -U pip wheel pip install --progress-bar off ".[${{ matrix.spark }}, dev, doc]" - name: Run pytest run: | diff --git a/pyproject.toml b/pyproject.toml index b643f4af..30c30b99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,15 +30,14 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Operating System :: Unix", ] -requires-python = ">=3.7.1,<3.8" +requires-python = ">=3.7.1" dependencies = [ "pgpasslib>=1.1.0, <2.0.0", - "psycopg2-binary>=2.9.0, <3.0.0", + "psycopg2-binary>=2.9.0", "pandas>=1.3.0, <2.0.0", "numpy>=1.0.0", - "altair>=5.0.0, <6.0.0", + "altair>=5.0.0", "loguru==0.7.0", - "pypandoc==1.7.5", "pyspark", "pyarrow>=0.10.0", "pretty-html-table>=0.9.15, <0.10.0",