Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ print(ba_rehash)
## Bitarray similarity

`fable_core.similarity` contains functions for computing the similarity of bitarrays.
It implements the Dice coefficient, Cosine similarity and the Jaccard index.
The Dice coefficient, Cosine similarity and the Jaccard index are three examples of implemented similarity measures.
Have a look at the module to see what measures are currently implemented.

```python
from fable_core import similarity
Expand All @@ -173,27 +174,38 @@ print(ba_1)
print(ba_2)
# => bitarray('01001000111110011011100100101000')

# For all similarity functions, let n1 and n2 be the amount of set bits in ba_1 and ba_2 respectively,
# and let n12 be the amount of set bits in the intersection of ba_1 and ba_2.
# For all similarity functions, let c and b be the amount of set bits in either ba_1 or ba_2.
# Let a be the amount of set bits in the intersection and d the amount bits where both bitarrays are set to 0.

# In ba_1 and ba_2, there are only 3 positions where bits are set in both bitarrays. Each similarity
# function will treat this a bit differently.
print((ba_1 & ba_2).count())
# => 3

# Dice coefficient (2 * n12 / (n1 + n2))
# Dice coefficient (2 * a / (2 * a + b + c))
print(similarity.dice(ba_1, ba_2))
# => 0.2222222222222222

# Cosine similarity (n12 / sqrt(n1 * n2))
# Cosine similarity (a / sqrt((a + b) * (a + c)))
print(similarity.cosine(ba_1, ba_2))
# => 0.22360679774997896

# Jaccard index (n12 / (n1 + n2 - n12))
# Jaccard index (a / (a + b + c))
print(similarity.jaccard(ba_1, ba_2))
# => 0.125
```

## Similarity aggregation

`fable_core.aggregation` contains functions for aggregating multiple similarity into one.

```python
from fable_core import aggregation

print(aggregation.average(similarities=[0.5, 0.8], weights=[2, 1]))
# => 0.6
```

## String transformation

`fable_core.transform` contains factory functions for performing preprocessing on strings.
Expand Down
4 changes: 2 additions & 2 deletions fable_core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from . import bits, common, harden, phonetics_extra, similarity, transform
from . import bits, common, harden, phonetics_extra, similarity, transform, aggregation

__all__ = ["bits", "common", "harden", "phonetics_extra", "similarity", "transform"]
__all__ = ["bits", "common", "harden", "phonetics_extra", "similarity", "transform", "aggregation"]
46 changes: 46 additions & 0 deletions fable_core/aggregation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
__all__ = [
"AggregationFn",
"average",
"maximum",
"minimum",
]

from typing import Any, Protocol


class AggregationFn(Protocol):
def __call__(self, similarities: list[float], /, **kwargs: Any) -> float: ...


def _average(similarities: list[float], /, weights: list[float] | None = None) -> float:
"""
Compute the weighted average of a similarity vector.

Args:
similarities: list of similarities
weights: list of weights

Returns:
weighted average of similarities

Raises:
ValueError: if length of similarities does not equal length of weights
"""
if weights is None:
weights = [1] * len(similarities)
if len(weights) != len(similarities):
raise ValueError("There need to be as many weights as there are similarities.")
return sum(threshold * weight for threshold, weight in zip(similarities, weights, strict=True)) / sum(weights)


# Wrap aggregators so that the type checker does not complain.
def average(similarities: list[float], **kwargs: Any) -> float:
return _average(similarities, **kwargs)


def maximum(similarities: list[float], **kwargs: Any) -> float:
return max(similarities, **kwargs)


def minimum(similarities: list[float], **kwargs: Any) -> float:
return min(similarities, **kwargs)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "fable-core"
version = "0.1.5"
version = "0.2.0"
description = "Facilities required for performing privacy-preserving record linkage with Bloom filters in the FABLE ecosystem."
authors = [
{name = "Maximilian Jugl"},
Expand Down
22 changes: 22 additions & 0 deletions tests/test_aggregation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import pytest

from fable_core import aggregation


@pytest.mark.parametrize(
"similarities,weights,expected",
[
([0.6, 0.3, 0.6], None, 0.5),
([0.6, 0.3, 0.6], [1, 1, 1], 0.5),
([0.5, 0.8], [2, 1], 0.6),
],
)
def test_average_aggregation(similarities, weights, expected):
assert aggregation.average(similarities, weights=weights) == expected


def test_average_value_error():
with pytest.raises(ValueError) as e:
aggregation.average([0.5, 0.7, 0.8], weights=[2, 3.2])

assert "There need to be as many weights as there are similarities." in str(e.value)