Repository: tommyod/Efficient-Apriori

Issue:
Using iterator or generator to provide transactions
I use a generator to pass the transactions.
This works just fine if it actually returns any items.
However, if the generator or iterator is "empty", the application will crash:
```
File "...\.venv\lib\site-packages\efficient_apriori\itemsets.py", line 36, in __init__
    self._transactions = i + 1
UnboundLocalError: local variable 'i' referenced before assignment
```

A check for empty transactions is already done here but it does not work with generators or iterators:
https://github.com/tommyod/Efficient-Apriori/blob/96e89176b48a8255010c2951968894c22a1897e8/efficient_apriori/itemsets.py#L274

**My suggestion:**
Initialize `i` in `TransactionManager.__init__` as follows
```
    def __init__(self, transactions: typing.Iterable[typing.Iterable[typing.Hashable]]):

        # A lookup that returns indices of transactions for each item
        self._indices_by_item = collections.defaultdict(set)

        # Populate
        i = -1 # NEW
        for i, transaction in enumerate(transactions):
            for item in transaction:
                self._indices_by_item[item].add(i)

        # Total number of transactions
        self._transactions = i + 1
```
and add an additional check after [instantiating the class](https://github.com/tommyod/Efficient-Apriori/blob/96e89176b48a8255010c2951968894c22a1897e8/efficient_apriori/itemsets.py#L278):
```
def itemsets_from_transactions(...
    ...
    # Store in transaction manager
    manager = TransactionManager(transactions)

    # NEW: moved check below manager init and replaced the check by using no. of transactions
    if len(manager) < 1:
        return dict(), 0  # large_itemsets, num_transactions
    ...
```

I undestand that the expected input is a list, set or tuple but wrt. duck-typing and the (hopefully) little effort required to fix, maybe one can add it ;)

Thanks for considering and thanks as well for providing the library. 👍 

Discussion:
S1r0hub: Fixes #48
tommyod: Great work, thanks a lot!

Patch:
diff --git a/efficient_apriori/itemsets.py b/efficient_apriori/itemsets.py
index 368417d..e183718 100644
--- a/efficient_apriori/itemsets.py
+++ b/efficient_apriori/itemsets.py
@@ -28,6 +28,7 @@ class TransactionManager:
         self._indices_by_item = collections.defaultdict(set)
 
         # Populate
+        i = -1
         for i, transaction in enumerate(transactions):
             for item in transaction:
                 self._indices_by_item[item].add(i)
@@ -270,13 +271,14 @@ def itemsets_from_transactions(
     if not (isinstance(min_support, numbers.Number) and (0 <= min_support <= 1)):
         raise ValueError("`min_support` must be a number between 0 and 1.")
 
-    # If not transactions are present
-    if not transactions:
-        return dict(), 0  # large_itemsets, num_transactions
-
     # Store in transaction manager
     manager = TransactionManager(transactions)
 
+    # If no transactions are present
+    transaction_count = len(manager)
+    if transaction_count == 0:
+        return dict(), 0  # large_itemsets, num_transactions
+
     # STEP 1 - Generate all large itemsets of size 1
     # ----------------------------------------------
     if verbosity > 0:


Test patch:
diff --git a/efficient_apriori/tests/test_apriori.py b/efficient_apriori/tests/test_apriori.py
index fc5bb3a..21ff875 100644
--- a/efficient_apriori/tests/test_apriori.py
+++ b/efficient_apriori/tests/test_apriori.py
@@ -230,5 +230,23 @@ def test_minimal_input():
     itemsets, rules = apriori([(1, 2), (1, 2), (1, 3)], 1.0, 1.0)
 
 
+def test_iterator_input():
+    """
+    Minimal test using transactions from iterators.
+    """
+    empty_iterator = iter(())
+    transactions = empty_iterator
+    itemsets, rules = apriori(transactions, 0.2, 0.2)
+    assert itemsets == {} and rules == []
+
+    transactions = [(1, 2), (1, 2), (1, 3)]
+    transactions_iter = iter(transactions)
+    itemsets1, rules1 = apriori(transactions_iter, 1, 1)
+    itemsets2, rules2 = apriori(transactions, 1, 1)
+    assert len(rules1) == len(rules2)
+    for i in range(len(rules1)):
+        assert rules1[i] == rules2[i]
+
+
 if __name__ == "__main__":
     pytest.main(args=[".", "--doctest-modules", "-v"])