commit be5ed83ac95e1d76dac626e9a5216eabe16e25cf Author: AliceDarkest Date: Sun Jul 19 20:25:08 2026 +0200 Add public Cloudy profile matching sample diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43ae0e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.py[cod] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1593bb --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Profile Matching - Public Code Sample + +This is a small, dependency-free portfolio excerpt adapted from **Cloudy**, an +AI-assisted profile and matching service I built for a collaboration platform. +The production repository is private, so this project intentionally contains +only the self-contained scoring layer and its tests. It includes no user data, +credentials, API code, or project-specific infrastructure. + +## What it demonstrates + +- transparent, explainable matching across skills, interests, location, and + shared project tags; +- input normalisation and explicit handling of incomplete profile data; +- optional affinity weights that personalise recommendations without changing + existing behaviour when no preferences are set; +- pure business logic separated from database and API concerns; +- executable tests for normal cases, edge cases, and scoring invariants. + +The actual application combines this score with an asynchronous PostgreSQL +repository layer and uses it to rank potential collaboration matches. + +## Run the tests + +The sample uses only the Python standard library: + +```bash +python -m unittest -v +``` + +## Why this is good code + +The model is deliberately small and deterministic. Each function has one +responsibility, is type annotated, and can be tested without a database, LLM, +or network access. Missing location data never creates a false positive, and +the weighted extension remains backwards-compatible with the original Jaccard +similarity model. That keeps the recommendation logic understandable for users +and safe to evolve in a larger product. diff --git a/profile_matching.py b/profile_matching.py new file mode 100644 index 0000000..f7adfd1 --- /dev/null +++ b/profile_matching.py @@ -0,0 +1,95 @@ +"""Dependency-free, explainable scoring for profile recommendations. + +Adapted as a public portfolio excerpt from Cloudy's matching service. In the +application, an async repository layer supplies the profile attributes while +this module remains intentionally independent of persistence and APIs. +""" +from __future__ import annotations + +import math +from collections.abc import Iterable, Mapping + + +SKILL_WEIGHT = 0.30 +INTEREST_WEIGHT = 0.35 +LOCATION_WEIGHT = 0.15 +PROJECT_TAG_WEIGHT = 0.20 +LOCATION_DECAY_KM = 100.0 + + +def _normalise(items: Iterable[str]) -> set[str]: + """Return non-empty values as a case-insensitive set.""" + return {item.strip().casefold() for item in items if item and item.strip()} + + +def jaccard_similarity(left: Iterable[str], right: Iterable[str]) -> float: + """Return set similarity in the inclusive range from 0.0 to 1.0.""" + left_values = _normalise(left) + right_values = _normalise(right) + union = left_values | right_values + if not union: + return 0.0 + return len(left_values & right_values) / len(union) + + +def weighted_jaccard_similarity( + left: Iterable[str], + right: Iterable[str], + affinity: Mapping[str, float] | None = None, +) -> float: + """Personalise Jaccard similarity with optional preference weights. + + Values absent from ``affinity`` remain neutral at 1.0. Therefore omitting + preferences preserves the result of ``jaccard_similarity`` exactly. + """ + left_values = _normalise(left) + right_values = _normalise(right) + union = left_values | right_values + if not union: + return 0.0 + + weights = affinity or {} + shared_weight = sum(weights.get(value, 1.0) for value in left_values & right_values) + total_weight = sum(weights.get(value, 1.0) for value in union) + return shared_weight / total_weight if total_weight else 0.0 + + +def haversine_distance_km( + latitude_a: float, longitude_a: float, latitude_b: float, longitude_b: float +) -> float: + """Calculate the great-circle distance between two geographic coordinates.""" + earth_radius_km = 6_371.0 + lat_a, lon_a, lat_b, lon_b = map( + math.radians, (latitude_a, longitude_a, latitude_b, longitude_b) + ) + delta_lat = lat_b - lat_a + delta_lon = lon_b - lon_a + area = math.sin(delta_lat / 2) ** 2 + math.cos(lat_a) * math.cos(lat_b) * math.sin(delta_lon / 2) ** 2 + return earth_radius_km * 2 * math.asin(math.sqrt(area)) + + +def location_similarity( + left: tuple[float | None, float | None] | None, + right: tuple[float | None, float | None] | None, + decay_km: float = LOCATION_DECAY_KM, +) -> float: + """Turn distance into a linear score; incomplete coordinates score 0.0.""" + if left is None or right is None or None in (*left, *right): + return 0.0 + distance = haversine_distance_km(*left, *right) # type: ignore[arg-type] + return max(0.0, 1.0 - distance / decay_km) + + +def recommendation_score( + skill_similarity: float, + interest_similarity: float, + location_similarity_score: float, + project_tag_similarity: float, +) -> float: + """Combine the four transparent score components into one ranking value.""" + return ( + SKILL_WEIGHT * skill_similarity + + INTEREST_WEIGHT * interest_similarity + + LOCATION_WEIGHT * location_similarity_score + + PROJECT_TAG_WEIGHT * project_tag_similarity + ) diff --git a/test_profile_matching.py b/test_profile_matching.py new file mode 100644 index 0000000..a9eae9f --- /dev/null +++ b/test_profile_matching.py @@ -0,0 +1,68 @@ +"""Tests for the public profile-matching code sample.""" +from __future__ import annotations + +import unittest + +from profile_matching import ( + INTEREST_WEIGHT, + LOCATION_WEIGHT, + PROJECT_TAG_WEIGHT, + SKILL_WEIGHT, + haversine_distance_km, + jaccard_similarity, + location_similarity, + recommendation_score, + weighted_jaccard_similarity, +) + + +class ProfileMatchingTests(unittest.TestCase): + def test_component_weights_add_up_to_one(self) -> None: + self.assertAlmostEqual( + SKILL_WEIGHT + INTEREST_WEIGHT + LOCATION_WEIGHT + PROJECT_TAG_WEIGHT, + 1.0, + ) + + def test_similarity_normalises_case_whitespace_and_duplicates(self) -> None: + self.assertEqual( + jaccard_similarity([" Python ", "python", ""], ["PYTHON"]), 1.0 + ) + + def test_empty_profiles_do_not_create_a_match(self) -> None: + self.assertEqual(jaccard_similarity([], []), 0.0) + self.assertEqual(location_similarity(None, (51.34, 12.37)), 0.0) + + def test_unweighted_mode_is_backwards_compatible(self) -> None: + left, right = ["Python", "Rust"], ["Rust", "TypeScript"] + self.assertAlmostEqual( + weighted_jaccard_similarity(left, right), + jaccard_similarity(left, right), + ) + + def test_shared_high_affinity_interest_increases_match_score(self) -> None: + neutral = weighted_jaccard_similarity( + ["Hiking", "Opera"], ["Opera", "Cooking"] + ) + personalised = weighted_jaccard_similarity( + ["Hiking", "Opera"], ["Opera", "Cooking"], {"opera": 3.0} + ) + self.assertGreater(personalised, neutral) + + def test_distance_is_symmetric_and_decays_to_zero(self) -> None: + leipzig = (51.3397, 12.3731) + berlin = (52.52, 13.405) + self.assertAlmostEqual( + haversine_distance_km(*leipzig, *berlin), + haversine_distance_km(*berlin, *leipzig), + ) + self.assertEqual(location_similarity(leipzig, berlin), 0.0) + + def test_final_score_is_explainable_weighted_sum(self) -> None: + self.assertAlmostEqual( + recommendation_score(0.5, 0.5, 1.0, 0.0), + 0.475, + ) + + +if __name__ == "__main__": + unittest.main()