Skip to content

Commit

Permalink
Modernize code base (#38)
Browse files Browse the repository at this point in the history
* Fix installation on non-UTF-8 platforms

Installation was broken on Windows,
because of non_ASCII characters in the README.

* Add Github actions for testing/coverage

Make sure that python-louain is installed for coverage.

* Remove travis

* Support Python versions 3.8-3.11

* Fix pytest deprecation warnings

We got a bunch of warnings in CI like this:

   tests/test_linkpred.py::TestLinkpred::test_predict_all
    /home/runner/work/linkpred/linkpred/.tox/py/lib/python3.11/site-packages/_pytest/fixtures.py:917: PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.
    tests/test_linkpred.py::TestLinkpred::test_predict_all is using nose-specific method: `teardown(self)`
    To remove this warning, rename it to `teardown_method(self)`
    See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose

* Add ruff and fix a bunch of ruff warnings

* Fix various deprecation warnings

This introduced a bug during development (sparse matrix vs sparse
array).
Since scipy 1.10, the power operator ** performs elementwise
multiplications, instead of matrix multiplication. In this change, we
implement a small recursive function to reinstate the old behaviour,
such that the Katz predictor can rely on it again.

Fixes #37

* Bump dependency versions
  • Loading branch information
rafguns committed Mar 15, 2023
1 parent dbd0318 commit 2a0ae94
Show file tree
Hide file tree
Showing 36 changed files with 336 additions and 232 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Calculate test coverage

on: [push]

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.10"
- uses: actions/cache@v2
name: Configure pip caching
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install Python dependencies
run: |
pip install --upgrade pip
pip install -e .[community]
pip install pytest pytest-cov
- name: Run tests
run: |-
pytest --cov=linkpred --cov-report xml:coverage.xml --cov-report term
- name: Upload coverage report
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: coverage.xml
31 changes: 31 additions & 0 deletions .github/workflows/tox.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Test with tox

on: [push]

jobs:
build:
runs-on: ubuntu-latest
strategy:
max-parallel: 5
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v2
name: Configure pip caching
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox
25 changes: 0 additions & 25 deletions .travis.yml

This file was deleted.

19 changes: 8 additions & 11 deletions linkpred/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import logging
import sys

import yaml

from .exceptions import LinkPredError
from .linkpred import LinkPred
from .predictors import all_predictors
Expand All @@ -26,17 +28,12 @@ def load_profile(fname):
"""Load the JSON or YAML profile with the given filename"""
try:
with open(fname) as f:
if fname.endswith(".yaml") or fname.endswith(".yml"):
import yaml

if fname.endswith((".yaml", ".yml")):
return yaml.safe_load(f)
else:
return json.load(f)
except Exception as e:
raise LinkPredError(
"Encountered error while loading profile '%s'. "
"Error message: '%s'" % (fname, e)
)
return json.load(f)
except Exception as err:
msg = f"Encountered error while loading profile '{fname}'. "
raise LinkPredError(msg) from err


def get_config(args=None):
Expand Down Expand Up @@ -142,7 +139,7 @@ def handle_arguments(args=None):
metavar="PREDICTOR",
)

all_help = "Predict all links, " "including ones present in the training network"
all_help = "Predict all links, including ones present in the training network"
parser.add_argument(
"-a",
"--all",
Expand Down
46 changes: 23 additions & 23 deletions linkpred/evaluation/listeners.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import copy
import logging
import smokesignal

from time import localtime, strftime

from .static import EvaluationSheet
import smokesignal

from ..util import interpolate
from .static import EvaluationSheet

log = logging.getLogger(__name__)

Expand All @@ -28,7 +28,7 @@ def _timestamped_filename(basename, ext="txt"):
return basename + strftime("_%Y-%m-%d_%H.%M.", localtime()) + ext


class Listener(object):
class Listener:
def __init__(self):
smokesignal.on("dataset_finished", self.on_dataset_finished)
smokesignal.on("run_finished", self.on_run_finished)
Expand All @@ -45,7 +45,7 @@ def __init__(self, **kwargs):
smokesignal.on("prediction_finished", self.on_prediction_finished)
self.params = kwargs

super(EvaluatingListener, self).__init__()
super().__init__()

def on_prediction_finished(self, scoresheet, dataset, predictor):
evaluation = EvaluationSheet(scoresheet, **self.params)
Expand All @@ -60,21 +60,21 @@ def on_prediction_finished(self, scoresheet, dataset, predictor):
class CachePredictionListener(Listener):
def __init__(self):
smokesignal.on("prediction_finished", self.on_prediction_finished)
super(CachePredictionListener, self).__init__()
super().__init__()
self.encoding = "utf-8"

def on_prediction_finished(self, scoresheet, dataset, predictor):
self.fname = _timestamped_filename("%s-%s-predictions" % (dataset, predictor))
self.fname = _timestamped_filename(f"{dataset}-{predictor}-predictions")
scoresheet.to_file(self.fname)


class CacheEvaluationListener(Listener):
def __init__(self):
smokesignal.on("evaluation_finished", self.on_evaluation_finished)
super(CacheEvaluationListener, self).__init__()
super().__init__()

def on_evaluation_finished(self, evaluation, dataset, predictor):
self.fname = _timestamped_filename("%s-%s-predictions" % (dataset, predictor))
self.fname = _timestamped_filename(f"{dataset}-{predictor}-predictions")
evaluation.to_file(self.fname)


Expand All @@ -84,16 +84,16 @@ def __init__(self, name, beta=1):
self.fname = _timestamped_filename("%s-Fmax" % name)

smokesignal.on("evaluation_finished", self.on_evaluation_finished)
super(FMaxListener, self).__init__()
super().__init__()

def on_evaluation_finished(self, evaluation, dataset, predictor):
fmax = evaluation.f_score(self.beta).max()

status = "%s\t%s\t%.4f\n" % (dataset, predictor, fmax)
status = f"{dataset}\t{predictor}\t{fmax:.4f}\n"

with open(self.fname, "a") as f:
f.write(status)
print(status)
log.info("Evaluation finished: %s", status)


class PrecisionAtKListener(Listener):
Expand All @@ -102,15 +102,15 @@ def __init__(self, name, k=10):
self.fname = _timestamped_filename("%s-precision-at-%d" % (name, self.k))

smokesignal.on("evaluation_finished", self.on_evaluation_finished)
super(PrecisionAtKListener, self).__init__()
super().__init__()

def on_evaluation_finished(self, evaluation, dataset, predictor):
precision = evaluation.precision()[self.k]

status = "%s\t%s\t%.4f\n" % (dataset, predictor, precision)
status = f"{dataset}\t{predictor}\t{precision:.4f}\n"
with open(self.fname, "a") as f:
f.write(status)
print(status)
log.info("Evaluation finished: %s", status)


GENERIC_CHART_LOOKS = [
Expand Down Expand Up @@ -157,14 +157,14 @@ def __init__(self, name, xlabel="", ylabel="", filetype="pdf", chart_looks=None)
self._y = []

smokesignal.on("evaluation_finished", self.on_evaluation_finished)
super(Plotter, self).__init__()
super().__init__()

def add_line(self, predictor=""):
ax = self.fig.axes[0]
ax.plot(self._x, self._y, self.chart_look(), label=predictor)

log.debug(
"Added line with %d points: " "start = (%.2f, %.2f), end = (%.2f, %.2f)",
"Added line with %d points: start = (%.2f, %.2f), end = (%.2f, %.2f)",
len(self._x),
self._x[0],
self._y[0],
Expand All @@ -190,16 +190,16 @@ def on_run_finished(self):

# Save to file
self.fname = _timestamped_filename(
"%s-%s" % (self.name, self._charttype), self.filetype
f"{self.name}-{self._charttype}", self.filetype
)
self.fig.savefig(self.fname)


class RecallPrecisionPlotter(Plotter):
def __init__(
self, name, xlabel="Recall", ylabel="Precision", interpolation=True, **kwargs
self, name, xlabel="Recall", ylabel="Precision", *, interpolation=True, **kwargs
):
super(RecallPrecisionPlotter, self).__init__(name, xlabel, ylabel, **kwargs)
super().__init__(name, xlabel, ylabel, **kwargs)
self._charttype = "recall-precision"
self.interpolation = interpolation

Expand All @@ -215,7 +215,7 @@ def setup_coords(self, evaluation):

class FScorePlotter(Plotter):
def __init__(self, name, xlabel="#", ylabel="F-score", beta=1, **kwargs):
super(FScorePlotter, self).__init__(name, xlabel, ylabel, **kwargs)
super().__init__(name, xlabel, ylabel, **kwargs)
self._charttype = "F-Score"
self.beta = beta

Expand All @@ -228,7 +228,7 @@ class ROCPlotter(Plotter):
def __init__(
self, name, xlabel="False pos. rate", ylabel="True pos. rate", **kwargs
):
super(ROCPlotter, self).__init__(name, xlabel, ylabel, **kwargs)
super().__init__(name, xlabel, ylabel, **kwargs)
self._charttype = "ROC"

def setup_coords(self, evaluation):
Expand All @@ -238,7 +238,7 @@ def setup_coords(self, evaluation):

class MarkednessPlotter(Plotter):
def __init__(self, name, xlabel="Miss", ylabel="Precision", **kwargs):
super(MarkednessPlotter, self).__init__(name, xlabel, ylabel, **kwargs)
super().__init__(name, xlabel, ylabel, **kwargs)
self._charttype = "Markedness"
self._legend_props["loc"] = "upper left"

Expand Down
17 changes: 9 additions & 8 deletions linkpred/evaluation/scoresheet.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import networkx as nx

from collections import defaultdict

import networkx as nx
from networkx.readwrite.pajek import make_qstr

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -34,7 +34,7 @@ def __init__(self, data=None):
def __setitem__(self, key, val):
dict.__setitem__(self, key, float(val))

def process_data(self, data, *args, **kwargs):
def process_data(self, data):
"""Can be overridden by child classes"""
return data

Expand Down Expand Up @@ -75,7 +75,7 @@ def from_record(line, delimiter="\t"):
@staticmethod
def to_record(key, value, delimiter="\t"):
key, value = map(make_qstr, (key, value))
return u"{}{}{}\n".format(key, delimiter, value)
return f"{key}{delimiter}{value}\n"

@classmethod
def from_file(cls, fname, delimiter="\t", encoding="utf-8"):
Expand All @@ -94,7 +94,7 @@ def to_file(self, fname, delimiter="\t", encoding="utf-8"):
fh.write(self.to_record(key, score, delimiter).encode(encoding))


class Pair(object):
class Pair:
"""An unsorted pair of things.
We could probably also use frozenset for this, but a Pair class opens
Expand Down Expand Up @@ -122,9 +122,10 @@ def __init__(self, *args):
elif len(args) == 2:
a, b = args
else:
raise TypeError("__init__() takes 1 or 2 arguments in addition to self")
msg = "__init__() takes 1 or 2 arguments in addition to self"
raise TypeError(msg)
# For link prediction, a and b are two different nodes
assert a != b, "Predicted link (%s, %s) is a self-loop!" % (a, b)
assert a != b, f"Predicted link ({a}, {b}) is a self-loop!"
self.elements = self._sorted_tuple((a, b))

@staticmethod
Expand Down Expand Up @@ -216,4 +217,4 @@ def from_record(line, delimiter="\t"):
def to_record(key, value, delimiter="\t"):
u, v = key
u, v, score = map(make_qstr, (u, v, value))
return u"{0}{3}{1}{3}{2}\n".format(u, v, score, delimiter)
return f"{u}{delimiter}{v}{delimiter}{score}\n"
Loading

0 comments on commit 2a0ae94

Please sign in to comment.