Spaces:
Sleeping
Sleeping
Download tests/test_pattern_mining.py from parthnuwal7/ido: direct link, hf CLI and curl.
- Browser
- Download file 7.65 kB
-
https://huggingface.co/spaces/parthnuwal7/ido/resolve/main/tests/test_pattern_mining.py
- Command line
-
hf download hf://spaces/parthnuwal7/ido/tests/test_pattern_mining.py
-
curl -L -o test_pattern_mining.py https://huggingface.co/spaces/parthnuwal7/ido/resolve/main/tests/test_pattern_mining.py
7.65 kB
| """Tests for pattern_mining: association rules over watch history. | |
| The headline regression here is the channel-eligibility gate. It used to be | |
| max(10, len(watch_events)//100), which scales with total watches -- so a user with | |
| 20,626 watches needed 206 on a single channel to be considered, and got zero patterns | |
| while a user with 1,000 watches got plenty. Heavier use produced fewer insights. | |
| """ | |
| import collections | |
| import os | |
| import sys | |
| from datetime import datetime, timedelta | |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) | |
| from services.pattern_mining import ( # noqa: E402 | |
| MIN_SUPPORT, | |
| channel_day_rules, | |
| channel_slot_rules, | |
| cooccurrence_rules, | |
| find_patterns, | |
| opener_rules, | |
| seasonal_rules, | |
| top_channels, | |
| ) | |
| BASE = datetime(2026, 3, 1, 9, 0) # a Sunday | |
| def watch(channel, when): | |
| return { | |
| "type": "watch", | |
| "channel_clean": channel, | |
| "timestamp_local": when.isoformat(), | |
| "hour_local": when.hour, | |
| "day_of_week": when.weekday(), | |
| "month_local": when.month, | |
| "text_clean": f"{channel} video", | |
| } | |
| def spread(channel, count, start=BASE, step_days=7, hour=None): | |
| """`count` watches of one channel, one per step_days, optionally pinned to an hour.""" | |
| out = [] | |
| for i in range(count): | |
| when = start + timedelta(days=i * step_days) | |
| if hour is not None: | |
| when = when.replace(hour=hour) | |
| out.append(watch(channel, when)) | |
| return out | |
| # --- channel eligibility ---------------------------------------------------------- | |
| def test_gate_is_an_absolute_floor_not_a_proportion(): | |
| """The regression: a heavy watcher must not be gated out of their own patterns.""" | |
| events = spread("habit", 40) + [ | |
| watch(f"noise{i}", BASE + timedelta(hours=i)) for i in range(20000) | |
| ] | |
| assert "habit" in top_channels(events) | |
| def test_channels_below_the_floor_are_excluded(): | |
| events = spread("rare", MIN_SUPPORT - 1) + spread("common", MIN_SUPPORT + 5) | |
| selected = top_channels(events) | |
| assert "common" in selected | |
| assert "rare" not in selected | |
| def test_no_channels_yields_empty_selection(): | |
| assert top_channels([]) == set() | |
| # --- individual rule types -------------------------------------------------------- | |
| def test_finds_a_day_of_week_habit(): | |
| """Every watch on a Sunday: confidence 1.0, well above the 1/7 baseline.""" | |
| events = spread("sunday show", 20) + spread("filler", 20, step_days=1) | |
| rules = channel_day_rules(events, {"sunday show", "filler"}) | |
| sunday = [r for r in rules if r["channel"] == "sunday show"] | |
| assert sunday, "expected a Sunday pattern" | |
| assert sunday[0]["day"] == "Sunday" | |
| assert sunday[0]["confidence"] == 1.0 | |
| def test_finds_a_time_of_day_habit(): | |
| events = spread("morning show", 20, step_days=1, hour=7) + spread( | |
| "evening show", 20, step_days=1, hour=20 | |
| ) | |
| rules = channel_slot_rules(events, {"morning show", "evening show"}) | |
| morning = [r for r in rules if r["channel"] == "morning show"] | |
| assert morning and morning[0]["slot"] == "morning" | |
| def test_finds_channels_watched_together_in_a_session(): | |
| """Two channels always in the same sitting -- the association the README promises.""" | |
| events = [] | |
| for i in range(20): | |
| start = BASE + timedelta(days=i) | |
| events.append(watch("artist a", start)) | |
| events.append(watch("artist b", start + timedelta(minutes=5))) | |
| # Enough unrelated sessions that the pair's base rate is low -- with only a handful | |
| # of sessions, two channels in half of them is barely above chance by construction. | |
| for i in range(180): | |
| events.append(watch(f"other {i}", BASE + timedelta(days=100 + i))) | |
| rules = cooccurrence_rules(events) | |
| pairs = {frozenset((r["channel_a"], r["channel_b"])) for r in rules} | |
| assert frozenset(("artist a", "artist b")) in pairs | |
| def test_cooccurrence_ignores_channels_that_never_share_a_session(): | |
| events = [] | |
| for i in range(20): | |
| events.append(watch("solo a", BASE + timedelta(days=i))) | |
| events.append(watch("solo b", BASE + timedelta(days=i, hours=6))) | |
| rules = cooccurrence_rules(events) | |
| assert not [ | |
| r for r in rules | |
| if {r["channel_a"], r["channel_b"]} == {"solo a", "solo b"} | |
| ] | |
| def test_finds_a_session_opening_channel(): | |
| events = [] | |
| for i in range(20): | |
| start = BASE + timedelta(days=i) | |
| events.append(watch("opener", start)) | |
| events.append(watch("follower", start + timedelta(minutes=10))) | |
| rules = opener_rules(events) | |
| assert rules and rules[0]["channel"] == "opener" | |
| # --- orchestration ---------------------------------------------------------------- | |
| def test_find_patterns_respects_the_limit(): | |
| events = [] | |
| for name in "abcdefgh": | |
| events += spread(f"channel {name}", 20, hour=7, step_days=1) | |
| assert len(find_patterns(events, limit=3)) <= 3 | |
| def test_find_patterns_sorts_strongest_first(): | |
| events = spread("strong", 30, step_days=7) + spread("weak", 30, step_days=1) | |
| patterns = find_patterns(events) | |
| if len(patterns) > 1: | |
| scores = [p["lift"] * p["confidence"] for p in patterns] | |
| assert scores == sorted(scores, reverse=True) | |
| def test_find_patterns_on_empty_input(): | |
| assert find_patterns([]) == [] | |
| def test_every_pattern_carries_its_evidence(): | |
| events = spread("sunday show", 25) | |
| for pattern in find_patterns(events): | |
| assert pattern["count"] >= 1 | |
| assert 0 < pattern["confidence"] <= 1 | |
| assert pattern["lift"] > 0 | |
| assert pattern["insight"] | |
| # --- rule quality ----------------------------------------------------------------- | |
| def test_a_channel_only_ever_watched_in_one_month_is_not_seasonal(): | |
| """A channel discovered in April and dropped is not a season -- it never had the | |
| chance to appear elsewhere, so concentration carries no information.""" | |
| discovered = [watch("short lived", datetime(2026, 4, d, 20)) for d in range(1, 26)] | |
| spread_out = [ | |
| watch("always on", datetime(2026, m, d, 20)) | |
| for m in range(1, 9) for d in (1, 10, 20) | |
| ] | |
| rules = seasonal_rules(discovered + spread_out, {"short lived", "always on"}) | |
| assert not [r for r in rules if r["channel"] == "short lived"] | |
| def test_a_channel_watched_year_round_but_peaking_is_seasonal(): | |
| """The real signal: present across the year, concentrated in its season.""" | |
| events = [] | |
| for month in range(1, 9): | |
| for day in range(1, 4): | |
| events.append(watch("sport", datetime(2026, month, day, 20))) | |
| for day in range(1, 31): | |
| events.append(watch("sport", datetime(2026, 4, day, 20))) | |
| # Background viewing spread evenly, so April is unremarkable overall and the | |
| # channel has something to deviate from. | |
| for month in range(1, 9): | |
| for day in range(1, 26): | |
| events.append(watch("background", datetime(2026, month, day, 20))) | |
| rules = seasonal_rules(events, {"sport", "background"}) | |
| assert rules and rules[0]["month"] == "April" | |
| def test_no_single_rule_type_floods_the_card(): | |
| """Five insights of the same shape read as a bug, not a discovery.""" | |
| events = [] | |
| for i in range(6): | |
| name = f"seasonal {i}" | |
| for month in range(1, 9): | |
| events += [watch(name, datetime(2026, month, d, 20)) for d in (1, 2)] | |
| events += [watch(name, datetime(2026, 7, d, 20)) for d in range(1, 26)] | |
| patterns = find_patterns(events, limit=5) | |
| kinds = collections.Counter(p["type"] for p in patterns) | |
| assert max(kinds.values()) <= 2, f"one type dominated: {kinds}" | |