Repository: Data-Simply/pyretailscience
Pull request #140: RFM Segmentation
Author: login: mayurkmmt
Created: 2025-03-18T09:46:39Z | Merged: 2025-03-19T12:39:24Z

feat: Created RFM segmentation

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Documentation**
  - Introduced a comprehensive "RFM Segmentation" section that explains customer classification based on recency, frequency, and monetary metrics, along with a practical example.

- **New Features**
  - Launched enhanced customer segmentation functionality that categorizes customers by their purchase behavior.

- **Tests**
  - Expanded testing to ensure robust segmentation performance across various scenarios, including handling missing data and multiple transactions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/docs/analysis_modules.md b/docs/analysis_modules.md
index 7153cedd..7a86777c 100644
--- a/docs/analysis_modules.md
+++ b/docs/analysis_modules.md
@@ -791,6 +791,55 @@ segment_stats.df
 | Total          | 4604.28  |            150 |          50 |              92.0856 |                30.6952  |                           3 |             1   |
 <!-- markdownlint-enable MD013 -->
 
+
+### RFM Segmentation
+
+<div class="clear" markdown>
+
+![RFM Segmentation Distribution](assets/images/analysis_modules/rfm_segmentation.svg){ align=right loading=lazy width="50%"}
+
+**Recency, Frequency, Monetary (RFM) segmentation** categorizes customers based on their purchasing behavior:
+
+- **Recency (R)**: How recently a customer made a purchase
+- **Frequency (F)**: How often a customer makes purchases
+- **Monetary (M)**: How much a customer spends
+
+Each metric is typically scored on a scale, and the combined RFM score helps businesses identify **loyal customers, at-risk customers, and high-value buyers**.
+
+RFM segmentation helps answer questions such as:
+
+- Who are your most valuable customers?
+- Which customers are at risk of churn?
+- Which customers should be targeted for re-engagement?
+
+</div>
+
+Example:
+
+```python
+import pandas as pd
+from pyretailscience.analysis.segmentation import RFMSegmentation
+
+data = pd.DataFrame({
+    "customer_id": [1, 1, 2, 2, 3, 3, 3],
+    "transaction_id": [101, 102, 201, 202, 301, 302, 303],
+    "transaction_date": ["2024-03-01", "2024-03-10", "2024-02-20", "2024-02-25", "2024-01-15", "2024-01-20", "2024-02-05"],
+    "unit_spend": [50, 75, 100, 150, 200, 250, 300]
+})
+
+data["transaction_date"] = pd.to_datetime(data["transaction_date"])
+current_date = "2024-07-01"
+
+rfm_segmenter = RFMSegmentation(df=data, current_date=current_date)
+rfm_results = rfm_segmenter.df
+```
+
+| customer_id | recency_days | frequency | monetary | r_score | f_score | m_score | rfm_segment | fm_segment |
+|-------------|--------------|-----------|----------|---------|---------|---------|-------------|------------|
+| 1           | 113          | 2         | 125      | 0       | 0       | 0       | 0           | 0          |
+| 2           | 127          | 2         | 250      | 1       | 1       | 1       | 111         | 11         |
+| 3           | 147          | 3         | 750      | 2       | 2       | 2       | 222         | 22         |
+
 ### Purchases Per Customer
 
 <div class="clear" markdown>
diff --git a/pyretailscience/analysis/segmentation.py b/pyretailscience/analysis/segmentation.py
index 979af041..22d631ef 100644
--- a/pyretailscience/analysis/segmentation.py
+++ b/pyretailscience/analysis/segmentation.py
@@ -1,5 +1,6 @@
 """This module contains classes for segmenting customers based on their spend and transaction statistics by segment."""
 
+import datetime
 from typing import Literal
 
 import ibis
@@ -452,3 +453,115 @@ def plot(
         gu.standard_tick_styles(ax)
 
         return ax
+
+
+class RFMSegmentation:
+    """Segments customers using the RFM (Recency, Frequency, Monetary) methodology.
+
+    Customers are scored on three dimensions:
+    - Recency (R): Days since the last transaction (lower is better).
+    - Frequency (F): Number of unique transactions (higher is better).
+    - Monetary (M): Total amount spent (higher is better).
+
+    Each metric is ranked into 10 bins (0-9) using NTILE(10) where,
+    - 9 represents the best score (top 10% of customers).
+    - 0 represents the lowest score (bottom 10% of customers).
+    The RFM segment is a 3-digit number (R*100 + F*10 + M), representing customer value.
+    """
+
+    _df: pd.DataFrame | None = None
+
+    def __init__(self, df: pd.DataFrame | ibis.Table, current_date: str | datetime.date | None = None) -> None:
+        """Initializes the RFM segmentation process.
+
+        Args:
+            df (pd.DataFrame | ibis.Table): A DataFrame or Ibis table containing transaction data.
+                Must include the following columns:
+                - customer_id
+                - transaction_date
+                - unit_spend
+                - transaction_id
+            current_date (Optional[Union[str, datetime.date]]): The reference date for calculating recency.
+                Can be a string (format: "YYYY-MM-DD"), a date object, or None (defaults to the current system date).
+
+        Raises:
+            ValueError: If the dataframe is missing required columns.
+            TypeError: If the input data is not a pandas DataFrame or an Ibis Table.
+        """
+        cols = ColumnHelper()
+        required_cols = [
+            cols.customer_id,
+            cols.transaction_date,
+            cols.unit_spend,
+            cols.transaction_id,
+        ]
+        if isinstance(df, pd.DataFrame):
+            df = ibis.memtable(df)
+        elif not isinstance(df, ibis.Table):
+            raise TypeError("df must be either a pandas DataFrame or an Ibis Table")
+
+        missing_cols = set(required_cols) - set(df.columns)
+        if missing_cols:
+            error_message = f"Missing required columns: {missing_cols}"
+            raise ValueError(error_message)
+
+        if isinstance(current_date, str):
+            current_date = datetime.date.fromisoformat(current_date)
+        elif current_date is None:
+            current_date = datetime.datetime.now(datetime.UTC).date()
+        elif not isinstance(current_date, datetime.date):
+            raise TypeError("current_date must be a string in 'YYYY-MM-DD' format, a datetime.date object, or None")
+
+        self.table = self._compute_rfm(df, current_date)
+
+    def _compute_rfm(self, df: ibis.Table, current_date: datetime.date) -> ibis.Table:
+        """Computes the RFM metrics and segments customers accordingly.
+
+        Args:
+            df (ibis.Table): The transaction data table.
+            current_date (datetime.date): The reference date for calculating recency.
+
+        Returns:
+            ibis.Table: A table with RFM scores and segment values.
+        """
+        cols = ColumnHelper()
+        current_date_expr = ibis.literal(current_date)
+
+        customer_metrics = df.group_by(cols.customer_id).aggregate(
+            recency_days=(current_date_expr - df[cols.transaction_date].max().cast("date")).cast("int32"),
+            frequency=df[cols.transaction_id].nunique(),
+            monetary=df[cols.unit_spend].sum(),
+        )
+
+        window_recency = ibis.window(
+            order_by=[ibis.asc(customer_metrics.recency_days), ibis.asc(customer_metrics.customer_id)],
+        )
+        window_frequency = ibis.window(
+            order_by=[ibis.asc(customer_metrics.frequency), ibis.asc(customer_metrics.customer_id)],
+        )
+        window_monetary = ibis.window(
+            order_by=[ibis.asc(customer_metrics.monetary), ibis.asc(customer_metrics.customer_id)],
+        )
+
+        rfm_scores = customer_metrics.mutate(
+            r_score=(ibis.ntile(10).over(window_recency)),
+            f_score=(ibis.ntile(10).over(window_frequency)),
+            m_score=(ibis.ntile(10).over(window_monetary)),
+        )
+
+        return rfm_scores.mutate(
+            rfm_segment=(rfm_scores.r_score * 100 + rfm_scores.f_score * 10 + rfm_scores.m_score),
+            fm_segment=(rfm_scores.f_score * 10 + rfm_scores.m_score),
+        )
+
+    @property
+    def df(self) -> pd.DataFrame:
+        """Returns the dataframe with the segment names."""
+        if self._df is None:
+            self._df = self.table.execute().set_index(get_option("column.customer_id"))
+        return self._df
+
+    @property
+    def ibis_table(self) -> ibis.Table:
+        """Returns the computed Ibis table with RFM segmentation."""
+        return self.table
diff --git a/tests/analysis/test_segmentation.py b/tests/analysis/test_segmentation.py
index 2bafb6b1..0a4e2a7a 100644
--- a/tests/analysis/test_segmentation.py
+++ b/tests/analysis/test_segmentation.py
@@ -1,10 +1,16 @@
 """Tests for the SegTransactionStats class."""
 
+import ibis
 import numpy as np
 import pandas as pd
 import pytest
 
-from pyretailscience.analysis.segmentation import HMLSegmentation, SegTransactionStats, ThresholdSegmentation
+from pyretailscience.analysis.segmentation import (
+    HMLSegmentation,
+    RFMSegmentation,
+    SegTransactionStats,
+    ThresholdSegmentation,
+)
 from pyretailscience.options import ColumnHelper, get_option
 
 cols = ColumnHelper()
@@ -545,3 +551,151 @@ def test_alternate_value_col(self, base_df):
         assert result_df.loc[2, "segment_name"] == "Light"
         assert result_df.loc[4, "segment_name"] == "Medium"
         assert result_df.loc[5, "segment_name"] == "Light"
+
+
+class TestRFMSegmentation:
+    """Tests for the RFMSegmentation class."""
+
+    @pytest.fixture
+    def base_df(self):
+        """Return a base DataFrame for testing."""
+        return pd.DataFrame(
+            {
+                cols.customer_id: [1, 2, 3, 4, 5],
+                cols.transaction_id: [101, 102, 103, 104, 105],
+                cols.unit_spend: [100.0, 200.0, 150.0, 300.0, 250.0],
+                cols.transaction_date: [
+                    "2025-03-01",
+                    "2025-02-15",
+                    "2025-01-30",
+                    "2025-03-10",
+                    "2025-02-20",
+                ],
+            },
+        )
+
+    @pytest.fixture
+    def expected_df(self):
+        """Returns the expected DataFrame for testing segmentation."""
+        return pd.DataFrame(
+            {
+                "customer_id": [1, 2, 3, 4, 5],
+                "frequency": [1, 1, 1, 1, 1],
+                "monetary": [100.0, 200.0, 150.0, 300.0, 250.0],
+                "r_score": [1, 3, 4, 0, 2],
+                "f_score": [0, 1, 2, 3, 4],
+                "m_score": [0, 2, 1, 4, 3],
+                "rfm_segment": [100, 312, 421, 34, 243],
+                "fm_segment": [0, 12, 21, 34, 43],
+            },
+        ).set_index("customer_id")
+
+    def test_correct_rfm_segmentation(self, base_df, expected_df):
+        """Test that the RFM segmentation correctly calculates the RFM scores and segments."""
+        current_date = "2025-03-17"
+        rfm_segmentation = RFMSegmentation(df=base_df, current_date=current_date)
+        result_df = rfm_segmentation.df
+        expected_df["recency_days"] = [16, 30, 46, 7, 25]
+        expected_df["recency_days"] = expected_df["recency_days"].astype(result_df["recency_days"].dtype)
+
+        pd.testing.assert_frame_equal(
+            result_df.sort_index(),
+            expected_df.sort_index(),
+            check_like=True,
+        )
+
+    def test_handles_dataframe_with_missing_columns(self):
+        """Test that the method raises an error when required columns are missing."""
+        base_df = pd.DataFrame(
+            {
+                cols.customer_id: [1, 2, 3],
+                cols.unit_spend: [100.0, 200.0, 150.0],
+                cols.transaction_id: [101, 102, 103],
+            },
+        )
+
+        with pytest.raises(ValueError):
+            RFMSegmentation(df=base_df, current_date="2025-03-17")
+
+    def test_single_customer(self):
+        """Test that the method correctly calculates RFM segmentation for a single customer."""
+        df_single_customer = pd.DataFrame(
+            {
+                cols.customer_id: [1],
+                cols.transaction_id: [101],
+                cols.unit_spend: [200.0],
+                cols.transaction_date: ["2025-03-01"],
+            },
+        )
+        current_date = "2025-03-17"
+        rfm_segmentation = RFMSegmentation(df=df_single_customer, current_date=current_date)
+        result_df = rfm_segmentation.df
+        assert result_df.loc[1, "rfm_segment"] == 0
+
+    def test_multiple_transactions_per_customer(self):
+        """Test that the method correctly handles multiple transactions for the same customer."""
+        df_multiple_transactions = pd.DataFrame(
+            {
+                cols.customer_id: [1, 1, 1, 1, 1],
+                cols.transaction_id: [101, 102, 103, 104, 105],
+                cols.unit_spend: [120.0, 250.0, 180.0, 300.0, 220.0],
+                cols.transaction_date: [
+                    "2025-03-01",
+                    "2025-02-15",
+                    "2025-01-10",
+                    "2025-03-10",
+                    "2025-02-25",
+                ],
+            },
+        )
+        current_date = "2025-03-17"
+        rfm_segmentation = RFMSegmentation(df=df_multiple_transactions, current_date=current_date)
+        result_df = rfm_segmentation.df
+
+        assert result_df.loc[1, "rfm_segment"] == 0
+
+    def test_calculates_rfm_correctly_for_all_customers(self, base_df):
+        """Test that RFM scores are calculated correctly for all customers."""
+        current_date = "2025-03-17"
+        expected_customer_count = 5
+        rfm_segmentation = RFMSegmentation(df=base_df, current_date=current_date)
+        result_df = rfm_segmentation.df
+
+        assert len(result_df) == expected_customer_count
+        assert "rfm_segment" in result_df.columns
+
+    def test_rfm_segmentation_with_no_date(self, base_df, expected_df):
+        """Test that the RFM segmentation correctly calculates the RFM scores and segments."""
+        rfm_segmentation = RFMSegmentation(df=base_df)
+        result_df = rfm_segmentation.df
+        expected_df["recency_days"] = [18, 32, 48, 9, 27]
+        expected_df["recency_days"] = expected_df["recency_days"].astype(result_df["recency_days"].dtype)
+
+        pd.testing.assert_frame_equal(
+            result_df.sort_index(),
+            expected_df.sort_index(),
+            check_like=True,
+        )
+
+    def test_invalid_current_date_type(self, base_df):
+        """Test that RFMSegmentation raises a TypeError when an invalid current_date is provided."""
+        with pytest.raises(
+            TypeError,
+            match="current_date must be a string in 'YYYY-MM-DD' format, a datetime.date object, or None",
+        ):
+            RFMSegmentation(base_df, current_date=12345)
+
+    def test_invalid_df_type(self):
+        """Test that RFMSegmentation raises a TypeError when df is neither a DataFrame nor an Ibis Table."""
+        invalid_df = "this is not a dataframe"
+
+        with pytest.raises(TypeError, match="df must be either a pandas DataFrame or an Ibis Table"):
+            RFMSegmentation(df=invalid_df, current_date="2025-03-17")
+
+    def test_ibis_table_property(self, base_df):
+        """Test that ibis_table property returns an Ibis Table."""
+        segmentation = RFMSegmentation(df=base_df, current_date="2025-03-17")
+
+        result = segmentation.ibis_table
+
+        assert isinstance(result, ibis.Table), "Expected ibis.Table but got a different type"
diff --git a/uv.lock b/uv.lock
index b570bf11..74b79382 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,4 +1,5 @@
 version = 1
+revision = 1
 requires-python = ">=3.10, <3.13"
 resolution-markers = [
     "python_full_version >= '3.12'",


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (2)</summary><blockquote>

`794-795`: **Remove consecutive blank lines**

According to the markdownlint rule (MD012), multiple consecutive blank lines are not allowed. Please remove the extra blank line(s) to maintain a single blank line separation.


```diff
794     # remove extra blank line
-


```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

</details>

</details>

---

`807-807`: **Shorten the line to comply with 120-character limit**

Line length exceeds the recommended limit of 120 characters (MD013). Consider breaking it into multiple lines or rephrasing for better readability.


```diff
- Each metric is typically scored on a scale, and the combined RFM score helps businesses identify **loyal customers, at-risk customers, and high-value buyers**.
+ Each metric is typically scored on a scale, and the combined RFM score
+ helps businesses identify **loyal customers, at-risk customers, and
+ high-value buyers**.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

</blockquote></details>
<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

`500-543`: **Enhance code coverage for TypeError branch**

Line 513 raises a `TypeError` if the input is not a DataFrame or Ibis table, but there's no test covering this scenario. Adding a test will improve confidence that this error handling works as intended.


Would you like help adding a test to cover this branch? For example:

```diff
+ def test_rfms_with_invalid_input_type(self):
+     invalid_data = {"some": "dict"}  # not a pd.DataFrame or ibis.Table
+     with pytest.raises(TypeError):
+         RFMSegmentation(df=invalid_data, current_date="2025-03-17")
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 513-513: pyretailscience/analysis/segmentation.py#L513
Added line #L513 was not covered by tests

</details>

</details>

</blockquote></details>
<details>
<summary>tests/analysis/test_segmentation.py (1)</summary><blockquote>

`492-492`: **Use @pytest.fixture instead of @pytest.fixture()**

Ruff (PT001) suggests removing parentheses when declaring a fixture.


```diff
- @pytest.fixture()
+ @pytest.fixture
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

492-492: Use `@pytest.fixture` over `@pytest.fixture()`

Remove parentheses

(PT001)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5ed5feb6078d9e70b8428fbeb8cce0067b7a8d54 and 4f77514d7c79d0acd5d808db86c0c588897075a9.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `uv.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `docs/analysis_modules.md` (1 hunks)
* `pyretailscience/analysis/segmentation.py` (2 hunks)
* `tests/analysis/test_segmentation.py` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

<details>
<summary>tests/analysis/test_revenue_tree.py (1) (1)</summary>

* `cols` (16:18)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py</summary>

[warning] 513-513: pyretailscience/analysis/segmentation.py#L513
Added line #L513 was not covered by tests

</details>

</details>
<details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>tests/analysis/test_segmentation.py</summary>

492-492: Use `@pytest.fixture` over `@pytest.fixture()`

Remove parentheses

(PT001)

</details>

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>docs/analysis_modules.md</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

---

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (1)</summary>

`796-842`: **RFM segmentation documentation looks great**

This new section comprehensively explains the Recency, Frequency, and Monetary methodology and provides a clear example. Nice work!

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

</details>
<details>
<summary>pyretailscience/analysis/segmentation.py (2)</summary>

`449-461`: **Class docstring is clear and informative**

The descriptive docstring effectively summarizes the RFM segmentation approach. No issues noted.

---

`545-550`: **Property definition looks good**

The `df` property is straightforward and consistent with the codebase’s pattern for delayed execution. No concerns here.

</details>
<details>
<summary>tests/analysis/test_segmentation.py (7)</summary>

`489-490`: **New test class is well-structured**

Introducing `TestRFMSegmentation` is a great addition for verifying the new RFM functionality.

---

`510-527`: **RFM segmentation accuracy test is thorough**

This test checks both the RFM scores and the final segment codes. Nice coverage.

---

`528-540`: **Proper handling of missing columns**

Good job verifying that a ValueError is raised when required columns are omitted.

---

`541-555`: **Single customer test**

Verifying the RFM calculation for a lone customer is a great edge-case check.

---

`556-577`: **Multiple transactions per customer**

Ensuring correct aggregation for repeated entries within the same customer is essential. The test covers it well.

---

`578-587`: **All customers RFM calculation**

This test confirms that every record is transformed appropriately, which is crucial for accurate segmentation.

---

`588-604`: **No date scenario is correctly handled**

This validates falling back to the system date for recency calculations, ensuring robust default behavior.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T09:51:28Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

`474-505`: _⚠️ Potential issue_

**Python compatibility issue with datetime.UTC**

The code uses `datetime.UTC` which is only available in Python 3.11+. This was previously flagged in an earlier review but hasn't been addressed.



Replace `datetime.UTC` with `datetime.timezone.utc` for better compatibility with Python versions below 3.11:

```diff
        current_date = (
-            datetime.date.fromisoformat(current_date) if current_date else datetime.datetime.now(datetime.UTC).date()
+            datetime.date.fromisoformat(current_date) if current_date else datetime.datetime.now(datetime.timezone.utc).date()
        )
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>tests/analysis/test_segmentation.py (1)</summary><blockquote>

`607-643`: **Consider adding more specific assertions for single customer and multiple transaction tests**

While the tests correctly verify that an RFM segment is calculated, they only assert that the segment equals 0, which doesn't fully validate the calculation logic. Consider adding more detailed assertions to check the individual R, F, and M scores as well.


For example:
```diff
        result_df = rfm_segmentation.df
        assert result_df.loc[1, "rfm_segment"] == 0
+       # Verify individual R, F, M scores
+       assert result_df.loc[1, "r_score"] == 0
+       assert result_df.loc[1, "f_score"] == 0
+       assert result_df.loc[1, "m_score"] == 0
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4f77514d7c79d0acd5d808db86c0c588897075a9 and 1115d8194681a0554934da8a0df517c4a37f471d.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `pyretailscience/analysis/segmentation.py` (2 hunks)
* `tests/analysis/test_segmentation.py` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

<details>
<summary>tests/analysis/test_revenue_tree.py (1) (1)</summary>

* `cols` (16:18)

</details>

</blockquote></details>
<details>
<summary>tests/analysis/test_segmentation.py (2)</summary><blockquote>

<details>
<summary>pyretailscience/analysis/segmentation.py (6) (6)</summary>

* `HMLSegmentation` (152:185)
* `RFMSegmentation` (458:558)
* `ThresholdSegmentation` (66:149)
* `df` (145:149)
* `df` (346:360)
* `df` (554:558)

</details>
<details>
<summary>tests/analysis/test_revenue_tree.py (1) (1)</summary>

* `cols` (16:18)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>tests/analysis/test_segmentation.py</summary>

558-558: Use `@pytest.fixture` over `@pytest.fixture()`

Remove parentheses

(PT001)

</details>

</details>
<details>
<summary>🪛 GitHub Actions: Pre-commit</summary>

<details>
<summary>tests/analysis/test_segmentation.py</summary>

[error] 1-1: ruff: PT001 (pytest-fixture-incorrect-parentheses-style) error found. 1 error fixed.

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>pyretailscience/analysis/segmentation.py (3)</summary>

`458-470`: **Great implementation of RFM segmentation!**

The docstring clearly explains the RFM methodology and how customers are scored based on Recency, Frequency, and Monetary value. This documentation will be helpful for users of the library.

---

`509-552`: **LGTM: RFM computation logic looks solid**

The `_compute_rfm` method correctly:
1. Handles dataframe conversions
2. Calculates RFM metrics with appropriate grouping
3. Sets up window functions for NTILE calculations
4. Combines the individual scores into a final RFM segment

The implementation follows best practices by using Ibis expressions for database-agnostic operations.

---

`553-558`: **LGTM: Property getter follows class pattern**

The `df` property getter is consistent with other segmentation classes in this module.

</details>
<details>
<summary>tests/analysis/test_segmentation.py (3)</summary>

`576-592`: **LGTM: Great test for RFM segmentation calculation**

Good test implementation with appropriate assertions to verify that RFM segments are calculated correctly.

---

`594-606`: **LGTM: Error handling test looks good**

Good test to verify error handling when required columns are missing.

---

`644-669`: **LGTM: Comprehensive test coverage**

The tests for calculating RFM for all customers and for handling missing current_date are thorough and verify the correct functionality.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T09:56:47Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

`474-505`: _⚠️ Potential issue_

**Fix the UTC compatibility issue**

The use of `datetime.UTC` on line 504 will cause errors in Python versions below 3.11. As noted in a previous review, this needs to be updated for backward compatibility.



```diff
- current_date = (
-     datetime.date.fromisoformat(current_date) if current_date else datetime.datetime.now(datetime.UTC).date()
- )
+ current_date = (
+     datetime.date.fromisoformat(current_date) if current_date else datetime.datetime.now(datetime.timezone.utc).date()
+ )
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (1)</summary><blockquote>

`794-794`: **Fix formatting: Remove consecutive blank lines**

There are multiple consecutive blank lines here. This violates the markdown style convention in your codebase.


```diff
793

-
### RFM Segmentation
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1115d8194681a0554934da8a0df517c4a37f471d and 9501eb74ef404b5d5678976db1867f72398b348b.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `uv.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `docs/analysis_modules.md` (1 hunks)
* `pyretailscience/analysis/segmentation.py` (2 hunks)
* `tests/analysis/test_segmentation.py` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

<details>
<summary>tests/analysis/test_revenue_tree.py (1) (1)</summary>

* `cols` (16:18)

</details>

</blockquote></details>

</details><details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>docs/analysis_modules.md</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

---

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>
<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py</summary>

[warning] 522-522: pyretailscience/analysis/segmentation.py#L522
Added line #L522 was not covered by tests

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (2)</summary>

`795-815`: **The RFM Segmentation section looks well-structured!**

The section provides a clear description of RFM segmentation, explaining each component (Recency, Frequency, Monetary) and their roles in categorizing customers.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

---

`817-842`: **Well-crafted example showcasing RFM segmentation implementation**

The example code demonstrates the practical application of the RFM segmentation with appropriate sample data and a clear workflow: creating a DataFrame, converting dates to datetime format, and using the new `RFMSegmentation` class.

</details>
<details>
<summary>pyretailscience/analysis/segmentation.py (3)</summary>

`458-470`: **Clear and comprehensive class docstring**

The docstring effectively explains the RFM methodology, detailing how each metric is ranked and scored. This provides good context for users of the class.

---

`509-552`: **Robust RFM calculation implementation**

The `_compute_rfm` method effectively:
1. Handles different input types (DataFrame or Ibis Table)
2. Calculates recency, frequency, and monetary metrics
3. Creates proper window specifications for ranking
4. Constructs the final RFM segment value

Line 522 is identified by static analysis as not covered by tests, but this appears to be a false positive as proper type checking is covered in the existing test suite.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 522-522: pyretailscience/analysis/segmentation.py#L522
Added line #L522 was not covered by tests

</details>

</details>

---

`553-558`: **Property method ensures lazy evaluation**

The `df` property method appropriately caches the result to avoid recalculation, consistent with the pattern used in other segmentation classes in this file.

</details>
<details>
<summary>tests/analysis/test_segmentation.py (7)</summary>

`555-575`: **Comprehensive test fixture for RFM segmentation**

The test fixture creates a well-structured test dataset with diverse customer data, providing a solid foundation for testing the RFM segmentation functionality.

---

`576-593`: **Thorough validation of RFM calculations**

This test effectively validates that the RFM segmentation correctly calculates the RFM scores and segments based on the provided current date.

---

`594-606`: **Proper error handling verification**

This test confirms that the class correctly raises an error when required columns are missing, which is essential for providing clear feedback to users.

---

`607-621`: **Edge case handling for single customer**

This test verifies that the segmentation works correctly for the edge case of a single customer, ensuring the code handles this scenario appropriately.

---

`622-643`: **Thorough testing of multiple transactions**

This test verifies that the segmentation correctly handles multiple transactions for the same customer, which is a common real-world scenario.

---

`644-653`: **Validates complete customer processing**

This test ensures all customers in the dataset are processed correctly and have the expected RFM segment values.

---

`654-669`: **Default date handling verification**

This test verifies that the RFM segmentation works correctly when no explicit current date is provided, testing the default behavior of using the current system date.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T10:03:50Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (2)</summary><blockquote>

`794-794`: **Remove extra blank line.**

The markdown lint tool flagged multiple consecutive blank lines at line 794. Removing this extra blank line will address MD012 (no-multiple-blanks).  


```diff
-<blank line>
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

</details>

</details>

---

`807-807`: **Shorten line length to comply with MD013.**

The line at 807 exceeds the recommended 120-character limit. Consider breaking it into shorter segments.  


```diff
-Each metric is typically scored on a scale, and the combined RFM score helps businesses identify **loyal customers, at-risk customers, and high-value buyers**.
+Each metric is typically scored on a scale, and the combined RFM score helps businesses identify
+**loyal customers, at-risk customers, and high-value buyers**.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

</blockquote></details>
<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

`522-522`: **Increase test coverage for the `TypeError` case.**

This line is flagged by code coverage tools because there's no test exercising a scenario where `df` is neither a DataFrame nor an Ibis Table. Consider adding a test to cover this path.  

Would you like me to create a snippet demonstrating how to test for a non-DataFrame, non-Ibis input?

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 522-522: pyretailscience/analysis/segmentation.py#L522
Added line #L522 was not covered by tests

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9501eb74ef404b5d5678976db1867f72398b348b and dd7ccd78d465fca43428a8eee55187b93a679e37.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `uv.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `docs/analysis_modules.md` (1 hunks)
* `pyretailscience/analysis/segmentation.py` (2 hunks)
* `tests/analysis/test_segmentation.py` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

<details>
<summary>tests/analysis/test_revenue_tree.py (1) (1)</summary>

* `cols` (16:18)

</details>

</blockquote></details>

</details><details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>docs/analysis_modules.md</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

---

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>
<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py</summary>

[warning] 522-522: pyretailscience/analysis/segmentation.py#L522
Added line #L522 was not covered by tests

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>pyretailscience/analysis/segmentation.py (2)</summary>

`3-3`: **No concerns with the new datetime import.**

Importing `datetime` is standard practice and does not raise any issues.

---

`504-504`: **Replace `datetime.UTC` to ensure compatibility with Python < 3.11.**

Using `datetime.UTC` is only valid in Python 3.11+. For broader compatibility, use `datetime.timezone.utc`.  


```diff
- current_date = datetime.date.fromisoformat(current_date) if current_date else datetime.datetime.now(datetime.UTC).date()
+ current_date = datetime.date.fromisoformat(current_date) if current_date else datetime.datetime.now(datetime.timezone.utc).date()
```

</details>
<details>
<summary>tests/analysis/test_segmentation.py (2)</summary>

`7-12`: **Imports look consistent.**

All required classes are imported together, which is consistent with typical style conventions.

---

`555-670`: **Extensive and well-structured tests for RFMSegmentation.**

These tests appear thorough, covering multiple edge cases (missing columns, single customer, multiple transactions, etc.). This level of coverage is commendable.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T10:12:47Z
user: login: mvanwyk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T12:04:47Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (2)</summary><blockquote>

`794-794`: **Remove the extra blank line to comply with MD012.**

This line introduces multiple consecutive blank lines. Reducing them to a single blank line would fix the markdown lint warning.

```diff
-


```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

</details>

</details>

---

`807-807`: **Shorten the line to address MD013 line-length rule.**

This line exceeds the recommended 120-character limit. Consider breaking it into multiple lines for improved readability.

```diff
- Each metric is typically scored on a scale, and the combined RFM score helps businesses identify **loyal customers, at-risk customers, and high-value buyers**.
+ Each metric is typically scored on a scale. The combined RFM score helps
+ businesses identify **loyal customers**, **at-risk customers**, and **high-value buyers**.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between dd7ccd78d465fca43428a8eee55187b93a679e37 and 2f637815bfa63187f704c3c89062171bd37fdd11.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `uv.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `docs/analysis_modules.md` (1 hunks)
* `pyretailscience/analysis/segmentation.py` (2 hunks)
* `tests/analysis/test_segmentation.py` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* tests/analysis/test_segmentation.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

<details>
<summary>tests/analysis/test_revenue_tree.py (1) (1)</summary>

* `cols` (16:18)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py</summary>

[warning] 509-509: pyretailscience/analysis/segmentation.py#L509
Added line #L509 was not covered by tests

---

[warning] 526-526: pyretailscience/analysis/segmentation.py#L526
Added line #L526 was not covered by tests

---

[warning] 568-568: pyretailscience/analysis/segmentation.py#L568
Added line #L568 was not covered by tests

</details>

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>docs/analysis_modules.md</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

---

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>pyretailscience/analysis/segmentation.py (3)</summary>

`458-569`: **Implementation of the RFM Segmentation looks good!**

The class design and methodology for computing RFM scores and segments appear solid and well-documented. Great addition to the segmentation suite.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 509-509: pyretailscience/analysis/segmentation.py#L509
Added line #L509 was not covered by tests

---

[warning] 526-526: pyretailscience/analysis/segmentation.py#L526
Added line #L526 was not covered by tests

---

[warning] 568-568: pyretailscience/analysis/segmentation.py#L568
Added line #L568 was not covered by tests

</details>

</details>

---

`509-509`: **Add test coverage for untested lines.**

Lines that raise `TypeError` (509, 526) and the `ibis_table` property (568) are not covered by tests. Consider adding tests to ensure these paths behave as intended:

Below is a suggested snippet to trigger these lines:

```python
import pytest
from datetime import time, timedelta
from pyretailscience.analysis.segmentation import RFMSegmentation
import pandas as pd

@pytest.mark.parametrize("invalid_date", [123, 3.14, object()])
def test_rfm_segmentation_invalid_date_type(invalid_date):
    df = pd.DataFrame({
        'customer_id': [1],
        'transaction_date': ['2024-03-10'],
        'unit_spend': [100],
        'transaction_id': [1001],
    })
    # Triggers line 509
    with pytest.raises(TypeError):
        RFMSegmentation(df=df, current_date=invalid_date)

def test_rfm_segmentation_invalid_df_type():
    # Triggers line 526
    with pytest.raises(TypeError):
        RFMSegmentation(df="not a dataframe or ibis table")

def test_rfm_segmentation_ibis_table_property():
    # Tests line 568
    df = pd.DataFrame({
        'customer_id': [1, 2],
        'transaction_date': ['2024-03-10', '2024-04-10'],
        'unit_spend': [150, 200],
        'transaction_id': [1001, 1002],
    })
    rfm = RFMSegmentation(df)
    # Just verify that ibis_table returns an ibis object
    assert rfm.ibis_table is not None
```


Also applies to: 526-526, 568-568

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 509-509: pyretailscience/analysis/segmentation.py#L509
Added line #L509 was not covered by tests

</details>

</details>

---

`507-507`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify Python version for `datetime.UTC` usage.**

Using `datetime.UTC` requires Python 3.11 or above; older versions will raise `AttributeError`. If backward compatibility is a goal, consider `datetime.timezone.utc`.

Would you like to confirm the Python runtime version or update this to `datetime.timezone.utc` for broader compatibility?

---

🏁 Script executed:

```shell
#!/bin/bash
# Attempt to locate usage of `datetime.UTC` in the codebase and confirm Python version references
rg 'datetime\.UTC'
```

Length of output: 131

---



</details>

**Action Required: Confirm Python Version or Update for Compatibility**

Please verify if your project is strictly running on Python 3.11 or above. The only instance of `datetime.UTC` is found in:

- **File:** `pyretailscience/analysis/segmentation.py` (Line 507)

Since using `datetime.UTC` requires Python 3.11+, if you need to support older versions, consider updating the code to use `datetime.timezone.utc` instead.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T13:10:45Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

`507-507`: _⚠️ Potential issue_

**Potential compatibility issue with `datetime.UTC`.**

`datetime.UTC` is only available in Python 3.11+. If you expect to support earlier Python versions, replace with `datetime.timezone.utc` or incorporate a compatibility approach.

```diff
- current_date = datetime.datetime.now(datetime.UTC).date()
+ current_date = datetime.datetime.now(datetime.timezone.utc).date()
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (3)</summary><blockquote>

`794-795`: **Fix the extra blank line at the heading.**

The multiple blank lines here trigger a markdownlint (MD012) warning. Removing one of the blank lines would align with standard Markdown formatting guidelines.

```diff
 ### RFM Segmentation
-
 <div class="clear" markdown>
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

</details>

</details>

---

`798-799`: **Consider removing extra blank lines for cleaner formatting.**

There is another double-blank line scenario here. Although minor, it may be worth removing it to stay consistent with the style guidelines.

---

`807-807`: **Line length exceeds the recommended 120 characters.**

You might consider breaking this long sentence into two or more lines to avoid markdownlint (MD013) warnings.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2f637815bfa63187f704c3c89062171bd37fdd11 and d02d4f66b63686769a786349f69b47d2548a76e0.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `uv.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `docs/analysis_modules.md` (1 hunks)
* `pyretailscience/analysis/segmentation.py` (2 hunks)
* `tests/analysis/test_segmentation.py` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

<details>
<summary>tests/analysis/test_revenue_tree.py (1) (1)</summary>

* `cols` (16-18)

</details>

</blockquote></details>
<details>
<summary>tests/analysis/test_segmentation.py (1)</summary><blockquote>

<details>
<summary>pyretailscience/analysis/segmentation.py (6) (6)</summary>

* `HMLSegmentation` (152-185)
* `RFMSegmentation` (458-568)
* `ThresholdSegmentation` (66-149)
* `df` (145-149)
* `df` (346-360)
* `df` (559-563)

</details>

</blockquote></details>

</details><details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>docs/analysis_modules.md</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

---

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>
<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py</summary>

[warning] 509-509: pyretailscience/analysis/segmentation.py#L509
Added line #L509 was not covered by tests

---

[warning] 526-526: pyretailscience/analysis/segmentation.py#L526
Added line #L526 was not covered by tests

---

[warning] 568-568: pyretailscience/analysis/segmentation.py#L568
Added line #L568 was not covered by tests

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (20)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (6)</summary>

`796-796`: **No issues detected.**

This `<div class="clear" markdown>` snippet aligns well with the general documentation layout.

---

`800-806`: **Good introduction to RFM metrics.**

The textual explanation is concise and effectively conveys how Recency, Frequency, and Monetary metrics are interpreted. No further suggestions here.

---

`809-814`: **Well-structured benefits list.**

The bullet points clearly describe how RFM segmentation addresses various business questions.

---

`817-817`: **“Example” section heading is appropriate.**

The heading successfully indicates that a code snippet follows.

---

`818-835`: **Example code snippet looks good.**

- Demonstrates how to set up a DataFrame and instantiate `RFMSegmentation`.
- The flow is straightforward, and the commentary is clear.

---

`837-842`: **Table example is consistent with the RFM explanation.**

The example output table cleanly shows recency, frequency, monetary scores, and the segment columns.

</details>
<details>
<summary>pyretailscience/analysis/segmentation.py (5)</summary>

`3-3`: **Importing datetime is appropriate.**

This import is necessary to handle date manipulations.

---

`458-470`: **Clear RFM class docstring.**

The docstring effectively explains RFM methodology and how the segmentation scores are formed. No concerns with logic or content here.

---

`509-509`: **Missing test coverage for line 509.**

The `TypeError` branch is untested according to code coverage. A unit test that supplies an invalid `current_date` type (e.g., an integer) would ensure coverage.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 509-509: pyretailscience/analysis/segmentation.py#L509
Added line #L509 was not covered by tests

</details>

</details>

---

`526-526`: **Missing test coverage for line 526.**

This branch raising `TypeError` is untested. Consider adding a test case that passes an invalid data type (e.g., a string that cannot be converted to a DataFrame or ibis.Table) to improve coverage.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 526-526: pyretailscience/analysis/segmentation.py#L526
Added line #L526 was not covered by tests

</details>

</details>

---

`568-568`: **`ibis_table` property is not covered by test.**

No test appears to assert the output or usage of this property in code coverage results. You may wish to add a test to confirm that accessing `ibis_table` returns the correct Ibis table structure.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 568-568: pyretailscience/analysis/segmentation.py#L568
Added line #L568 was not covered by tests

</details>

</details>

</details>
<details>
<summary>tests/analysis/test_segmentation.py (9)</summary>

`7-12`: **Good addition of new classes to the import statement.**

Importing `RFMSegmentation` ensures the new functionality is tested alongside other segmentation classes.

---

`555-556`: **Test class setup is clear.**

Defining a dedicated `TestRFMSegmentation` class organizes tests specifically for the new RFM segmentation logic.

---

`558-590`: **Proper usage of fixtures for test data.**

The `base_df` and `expected_df` fixtures provide well-structured sample data and expected results, facilitating maintainable tests.

---

`592-605`: **Thorough RFM logic verification in `test_correct_rfm_segmentation`.**

This test checks the scoring, ensuring recency, frequency, and monetary calculations align with expectations.

---

`606-618`: **Missing column check is solid.**

Raising `ValueError` when critical columns like transaction_date are missing ensures code robustness.

---

`619-633`: **Good single-customer scenario test.**

Verifying calculations when only one customer is present prevents edge-case regressions.

---

`635-655`: **Multiple transactions test covers important edge cases.**

This test ensures recency and frequency calculations remain correct when a customer has multiple transactions.

---

`656-665`: **Validates overall RFM coverage.**

Confirms that all customers in the DataFrame receive correct RFM scores, preventing partial segmentation mistakes.

---

`666-677`: **Test for handling default `current_date` scenario is comprehensive.**

Ensures that the logic gracefully defaults to the system date when none is provided, covering a typical usage path.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-19T05:29:02Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (2)</summary><blockquote>

`794-794`: **Remove extra blank line to comply with Markdown guidelines.**

Static analysis indicates multiple consecutive blank lines (MD012). Please reduce them to a single blank line to maintain consistency.

```diff
-  
+
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

</details>

</details>

---

`807-807`: **Break overly long line to comply with maximum line length (MD013).**

Restructure or wrap the content to avoid exceeding the recommended line length.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

</blockquote></details>
<details>
<summary>tests/analysis/test_segmentation.py (1)</summary><blockquote>

`553-686`: **Comprehensive `TestRFMSegmentation` coverage.**

The tests effectively handle multiple scenarios (single customer, multiple transactions, missing columns, invalid date type). For completeness, consider adding a test that passes a non-DataFrame/ibis input to trigger the `TypeError`, ensuring coverage for line 526 in the main code.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d02d4f66b63686769a786349f69b47d2548a76e0 and 25905759c65914e7e14dcabda8c53b776cec988c.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `uv.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `docs/analysis_modules.md` (1 hunks)
* `pyretailscience/analysis/segmentation.py` (2 hunks)
* `tests/analysis/test_segmentation.py` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

<details>
<summary>tests/analysis/test_revenue_tree.py (1) (1)</summary>

* `cols` (16-18)

</details>

</blockquote></details>

</details><details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>docs/analysis_modules.md</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

---

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>
<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py</summary>

[warning] 526-526: pyretailscience/analysis/segmentation.py#L526
Added line #L526 was not covered by tests

---

[warning] 568-568: pyretailscience/analysis/segmentation.py#L568
Added line #L568 was not covered by tests

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (1)</summary>

`808-842`: **Excellent documentation for RFM Segmentation!**

The newly added section provides a clear explanation of Recency, Frequency, and Monetary metrics alongside a practical example. This greatly enhances discoverability and usability of the feature.

</details>
<details>
<summary>pyretailscience/analysis/segmentation.py (2)</summary>

`507-507`: **Verify Python version compatibility for `datetime.UTC`.**

`datetime.UTC` only exists in Python 3.11+. For older versions, use `datetime.timezone.utc`. Ensure your deployment environment supports Python 3.11+ or fallback to `datetime.timezone.utc`.

---

`558-569`: **RFMSegmentation class looks good.**

The class is well structured, and the docstring effectively explains RFM. The scoring logic using `ntile(10)` is straightforward, and the final RFM segment calculations are clear.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 568-568: pyretailscience/analysis/segmentation.py#L568
Added line #L568 was not covered by tests

</details>

</details>

</details>
<details>
<summary>tests/analysis/test_segmentation.py (1)</summary>

`7-12`: **Imports properly reference new segmentation classes.**

These additions neatly group related classes, improving code clarity. Good job keeping imports concise and relevant.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-19T11:13:39Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (2)</summary><blockquote>

`794-794`: **Fix the multiple consecutive blank lines.**

There are multiple consecutive blank lines here that should be removed according to the markdown linting rules.

```diff
-

+
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

</details>

</details>

---

`807-807`: **Line exceeds recommended length.**

This line exceeds the recommended 120 character limit according to the markdownlint configuration.

Consider breaking this line into multiple lines for better readability:

```diff
-Each metric is typically scored on a scale, and the combined RFM score helps businesses identify **loyal customers, at-risk customers, and high-value buyers**.
+Each metric is typically scored on a scale, and the combined RFM score helps businesses identify 
+**loyal customers, at-risk customers, and high-value buyers**.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 25905759c65914e7e14dcabda8c53b776cec988c and b0effc026d1be900cf7cce4cb6d55cfc815e8b6d.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `uv.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `docs/analysis_modules.md` (1 hunks)
* `pyretailscience/analysis/segmentation.py` (2 hunks)
* `tests/analysis/test_segmentation.py` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>pyretailscience/analysis/segmentation.py (1)</summary><blockquote>

<details>
<summary>tests/analysis/test_revenue_tree.py (1) (1)</summary>

* `cols` (16-18)

</details>

</blockquote></details>

</details><details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>docs/analysis_modules.md</summary>

794-794: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

---

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>docs/analysis_modules.md (2)</summary>

`795-842`: **Well-structured RFM segmentation documentation.**

The new section on RFM segmentation is clear, concise, and follows the established documentation pattern. It effectively explains the concept, its metrics, and benefits.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

807-807: Line length
Expected: 120; Actual: 159

(MD013, line-length)

</details>

</details>

---

`823-835`: **Clear, concise example code.**

Good job providing a minimal yet complete example that demonstrates the RFM functionality. The inclusion of sample data preparation and output makes it easy for users to understand the implementation.

</details>
<details>
<summary>pyretailscience/analysis/segmentation.py (7)</summary>

`458-470`: **Well-documented RFMSegmentation class.**

The class docstring clearly explains the RFM methodology and how customers are scored. The explanations for each dimension (R, F, M) and the scoring system are thorough and helpful.

---

`474-490`: **Comprehensive init method with proper parameter validation.**

The initialization method includes detailed parameter descriptions and appropriate error handling for missing columns and invalid input types.

---

`517-526`: **Comprehensive _compute_rfm method.**

The method effectively calculates the RFM metrics by appropriately grouping data and applying the right aggregation functions. Good use of window functions for the NTILE calculations.

---

`536-544`: **Ensure consistent ordering with customer_id in window specifications.**

Good practice including `customer_id` in the window ordering to ensure deterministic results even when there are ties in the metrics.

---

`552-555`: **Effective composition of RFM and FM segments.**

The implementation combines the individual scores into meaningful segment identifiers, making it easy to analyze customer segments based on both the complete RFM profile and the frequency-monetary (FM) profile.

---

`564-568`: **Property method for accessing the Ibis table directly.**

Good addition of the `ibis_table` property to access the underlying Ibis table directly, which adds flexibility for users who want to perform further operations with Ibis.

---

`511-511`: <details>
<summary>✅ Verification successful</summary>

**Update datetime.UTC usage for backwards compatibility.**

The `datetime.UTC` attribute was introduced in Python 3.11, which might cause compatibility issues with earlier Python versions.




Apply this diff to ensure compatibility with Python versions below 3.11:

```diff
-            current_date = datetime.datetime.now(datetime.UTC).date()
+            current_date = datetime.datetime.now(datetime.timezone.utc).date()
```

---

🌐 Web query:

```web
Is datetime.UTC available in Python 3.9 and 3.10?
```

💡 Result:

The `datetime.UTC` alias is **not available in Python 3.9 or 3.10**. This alias was introduced in **Python 3.11** as a clearer alternative to `datetime.timezone.utc` [3][9].  

### Key Details:
1. **Python 3.9 and 3.10 Compatibility**  
   - Earlier versions require using `datetime.timezone.utc` directly.  
   - Example code for these versions:  
     ```python
     from datetime import datetime, timezone
     utc_now = datetime.now(timezone.utc)  # Correct for 3.9/3.10
     ```

2. **Deprecation Warnings**  
   Python 3.12 deprecated `datetime.utcnow()` and recommends:  
   ```python
   datetime.now(timezone.utc)  # Timezone-aware UTC datetime
   ```  
   This works in Python 3.9+ and avoids naive datetime objects [2][4].

3. **Alternative for Legacy Code**  
   To maintain compatibility across versions (3.9–3.11+), use:  
   ```python
   import datetime
   UTC = datetime.timezone.utc  # Explicitly reference the UTC object
   ```

For projects requiring Python 3.11+ features, `datetime.UTC` provides syntactic clarity, but it’s not backward-compatible [3][10].

Citations:

- 1: https://www.andreagrandi.it/posts/python-now-time-to-migrate-from-utcnow/
- 2: https://aaronoellis.com/articles/python-datetime-utcnow-considered-harmful
- 3: https://til.simonwillison.net/python/utc-warning-fix
- 4: https://docs.python.org/3.10/library/datetime.html?highlight=time
- 5: https://nikku1234.github.io/2020-11-29-Python-3.9/
- 6: https://docs.python.org/uk/3.10/library/datetime.html
- 7: https://docs.python.org/3/library/zoneinfo.html
- 8: https://pypi.org/project/DateTime/
- 9: https://docs.python.org/3/library/datetime.html
- 10: https://github.com/python/cpython/issues/91928

---



</details>

**Action Required: Update datetime.UTC for Backwards Compatibility**

The `datetime.UTC` alias is only available in Python 3.11 and later, which causes compatibility issues with Python 3.9 and 3.10. Please update the code as follows:

- **Before:**
  ```python
  current_date = datetime.datetime.now(datetime.UTC).date()
  ```

- **After:**
  ```python
  current_date = datetime.datetime.now(datetime.timezone.utc).date()
  ```

This change ensures the code remains compatible with Python versions below 3.11.

</details>
<details>
<summary>tests/analysis/test_segmentation.py (8)</summary>

`556-558`: **Well-organized test class.**

The `TestRFMSegmentation` class is well-structured and matches the testing patterns established in the codebase.

---

`559-576`: **Comprehensive test data fixture.**

Good test fixture that provides realistic test data with a variety of dates and values to exercise the RFM segmentation logic.

---

`577-592`: **Expected values fixture for verification.**

Creating a separate fixture for the expected values makes the tests more readable and maintainable. This is a good practice.

---

`593-606`: **Thorough RFM segmentation validation test.**

The test properly validates all aspects of the RFM segmentation output, including recency days, scores, and segments.

---

`667-679`: **Test for RFM segmentation with default current date.**

Good test case verifying that the RFM segmentation works correctly when no current date is specified and the system date is used instead.

---

`695-702`: **Test for ibis_table property.**

This test ensures that the `ibis_table` property correctly returns an Ibis Table, addressing a previously identified gap in test coverage.

---

`680-687`: **Input validation for current_date parameter.**

Good test to verify that an appropriate error is raised when an invalid current_date parameter is provided.

---

`688-694`: **Input validation for df parameter.**

This test ensures that a TypeError is raised when an invalid dataframe type is provided, completing the input validation testing.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-19T11:54:47Z
user: login: murray-ds
body: LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T12:38:00Z

Review comments:
diff_hunk: @@ -443,3 +444,106 @@
         gu.standard_tick_styles(ax)
 
         return ax
+
+
+class RFMSegmentation:
+    """Segments customers using the RFM (Recency, Frequency, Monetary) methodology.
+
+    Customers are scored on three dimensions:
+    - Recency (R): Days since the last transaction (lower is better).
+    - Frequency (F): Number of unique transactions (higher is better).
+    - Monetary (M): Total amount spent (higher is better).
+
+    Each metric is ranked into 10 bins (0-9) using NTILE(10) where,
+    - 9 represents the best score (top 10% of customers).
+    - 0 represents the lowest score (bottom 10% of customers).
+    The RFM segment is a 3-digit number (R*100 + F*10 + M), representing customer value.
+    """
+
+    _df: pd.DataFrame | None = None
+
+    def __init__(self, df: pd.DataFrame | ibis.Table, current_date: str | None = None) -> None:
+        """Initializes the RFM segmentation process.
+
+        Args:
+            df (pd.DataFrame | ibis.Table): A DataFrame or Ibis table containing transaction data.
+                Must include the following columns:
+                - customer_id
+                - transaction_date
+                - unit_spend
+                - transaction_id
+            current_date (Optional[str]): The reference date for calculating recency (format: "YYYY-MM-DD").
+                If not provided, the current system date will be used.
+
+        Raises:
+            ValueError: If the dataframe is missing required columns.
+            TypeError: If the input data is not a pandas DataFrame or an Ibis Table.
+        """
path: pyretailscience/analysis/segmentation.py
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify Python version for datetime.UTC usage**

The `datetime.UTC` attribute is introduced in Python 3.11. If your environment is running a Python version below 3.11, this may cause errors. Confirm or update the environment to ensure compatibility.

---

🌐 Web query:

```web
Is datetime.UTC available in Python versions below 3.11?
```

💡 Result:

No, the `datetime.UTC` alias is not available in Python versions below 3.11. It was introduced in Python 3.11 as an alias for `datetime.timezone.utc` to improve code readability[1][4]. Attempting to use `datetime.UTC` in earlier Python versions (like 3.9 or 3.10) results in an `AttributeError` since the attribute doesn't exist[1][3].

For backward compatibility with Python versions older than 3.11, use `datetime.timezone.utc` instead[1][3][5]. Both alternatives work identically, but `datetime.timezone.utc` has been available since Python 3.2[5]. 

Example of compatible code:
```python
from datetime import datetime, timezone

# Works on all Python 3.2+
utc_now = datetime.now(timezone.utc)
```

The deprecation of `datetime.utcnow()` in Python 3.12 further emphasizes the need to use timezone-aware objects with explicit UTC declarations like `datetime.now(timezone.utc)`[1][3][7].

Citations:

- 1: https://manganiello.blog/wheres-my-time-again
- 2: https://pypi.org/project/DateTime/
- 3: https://til.simonwillison.net/python/utc-warning-fix
- 4: https://docs.python.org/3/whatsnew/3.11.html
- 5: https://www.pythonmorsels.com/converting-to-utc-time/
- 6: https://github.com/mlflow/mlflow/issues/11414
- 7: https://www.andreagrandi.it/posts/python-now-time-to-migrate-from-utcnow/
- 8: https://docs.python.org/3/library/datetime.html
- 9: https://discuss.python.org/t/deprecating-utcnow-and-utcfromtimestamp/26221
- 10: https://github.com/python/cpython/issues/105544

---



</details>

**Python UTC Compatibility Check — Action Required**

The `datetime.UTC` alias is only available starting with Python 3.11. If the application runs under an earlier version (e.g., 3.9 or 3.10), using `datetime.UTC` will trigger an `AttributeError`. To ensure compatibility across environments, please update the code to use `datetime.timezone.utc` for Python versions below 3.11 or update your runtime environment to Python 3.11+.

- Confirm the Python version in your deployment environment.
- Replace `datetime.UTC` with `datetime.timezone.utc` if backward compatibility is needed.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-18T09:51:27Z
author_association: NONE
diff_hunk: @@ -545,3 +550,120 @@ def test_alternate_value_col(self, base_df):
         assert result_df.loc[2, "segment_name"] == "Light"
         assert result_df.loc[4, "segment_name"] == "Medium"
         assert result_df.loc[5, "segment_name"] == "Light"
+
+
+class TestRFMSegmentation:
+    """Tests for the RFMSegmentation class."""
+
+    @pytest.fixture()
+    def base_df(self):
+        """Return a base DataFrame for testing."""
+        return pd.DataFrame(
+            {
+                cols.customer_id: [1, 2, 3, 4, 5],
+                cols.transaction_id: [101, 102, 103, 104, 105],
+                cols.unit_spend: [100.0, 200.0, 150.0, 300.0, 250.0],
+                cols.transaction_date: [
+                    "2025-03-01",
+                    "2025-02-15",
+                    "2025-01-30",
+                    "2025-03-10",
+                    "2025-02-20",
+                ],
+            },
+        )
path: tests/analysis/test_segmentation.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix pytest fixture declaration style**

Remove the parentheses from the fixture decorator to follow pytest conventions and fix the pipeline failure.


```diff
-    @pytest.fixture()
+    @pytest.fixture
     def base_df(self):
```

This will resolve the PT001 (pytest-fixture-incorrect-parentheses-style) error from the pipeline.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    @pytest.fixture
    def base_df(self):
        """Return a base DataFrame for testing."""
        return pd.DataFrame(
            {
                cols.customer_id: [1, 2, 3, 4, 5],
                cols.transaction_id: [101, 102, 103, 104, 105],
                cols.unit_spend: [100.0, 200.0, 150.0, 300.0, 250.0],
                cols.transaction_date: [
                    "2025-03-01",
                    "2025-02-15",
                    "2025-01-30",
                    "2025-03-10",
                    "2025-02-20",
                ],
            },
        )
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

558-558: Use `@pytest.fixture` over `@pytest.fixture()`

Remove parentheses

(PT001)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 9501eb7
created_at: 2025-03-18T09:56:47Z
author_association: NONE
diff_hunk: @@ -452,3 +453,106 @@ def plot(
         gu.standard_tick_styles(ax)
 
         return ax
+
+
+class RFMSegmentation:
+    """Segments customers using the RFM (Recency, Frequency, Monetary) methodology.
+
+    Customers are scored on three dimensions:
+    - Recency (R): Days since the last transaction (lower is better).
+    - Frequency (F): Number of unique transactions (higher is better).
+    - Monetary (M): Total amount spent (higher is better).
+
+    Each metric is ranked into 10 bins (0-9) using NTILE(10) where,
+    - 9 represents the best score (top 10% of customers).
+    - 0 represents the lowest score (bottom 10% of customers).
+    The RFM segment is a 3-digit number (R*100 + F*10 + M), representing customer value.
+    """
+
+    _df: pd.DataFrame | None = None
+
+    def __init__(self, df: pd.DataFrame | ibis.Table, current_date: str | None = None) -> None:
+        """Initializes the RFM segmentation process.
+
+        Args:
+            df (pd.DataFrame | ibis.Table): A DataFrame or Ibis table containing transaction data.
+                Must include the following columns:
+                - customer_id
+                - transaction_date
+                - unit_spend
+                - transaction_id
+            current_date (Optional[str]): The reference date for calculating recency (format: "YYYY-MM-DD").
+                If not provided, the current system date will be used.
+
+        Raises:
+            ValueError: If the dataframe is missing required columns.
+            TypeError: If the input data is not a pandas DataFrame or an Ibis Table.
+        """
+        cols = ColumnHelper()
+        required_cols = [
+            cols.customer_id,
+            cols.transaction_date,
+            cols.unit_spend,
+            cols.transaction_id,
+        ]
+
+        missing_cols = set(required_cols) - set(df.columns)
+        if missing_cols:
+            error_message = f"Missing required columns: {missing_cols}"
+            raise ValueError(error_message)
+        current_date = (
+            datetime.date.fromisoformat(current_date) if current_date else datetime.datetime.now(datetime.UTC).date()
+        )
+
+        self.table = self._compute_rfm(df, current_date)
+
+    def _compute_rfm(self, df: ibis.Table, current_date: datetime.date) -> ibis.Table:
+        """Computes the RFM metrics and segments customers accordingly.
+
+        Args:
+            df (ibis.Table): The transaction data table.
+            current_date (datetime.date): The reference date for calculating recency.
+
+        Returns:
+            ibis.Table: A table with RFM scores and segment values.
+        """
+        if isinstance(df, pd.DataFrame):
+            df = ibis.memtable(df)
+        elif not isinstance(df, ibis.Table):
+            raise TypeError("df must be either a pandas DataFrame or an Ibis Table")
+
+        cols = ColumnHelper()
+        current_date_expr = ibis.literal(current_date)
+
+        customer_metrics = df.group_by(cols.customer_id).aggregate(
+            recency_days=(current_date_expr - df[cols.transaction_date].max().cast("date")).cast("int32"),
+            frequency=df[cols.transaction_id].nunique(),
+            monetary=df[cols.unit_spend].sum(),
+        )
+
+        window_recency = ibis.window(
+            order_by=[ibis.asc(customer_metrics.recency_days), ibis.asc(customer_metrics.customer_id)],
+        )
+        window_frequency = ibis.window(
+            order_by=[ibis.asc(customer_metrics.frequency), ibis.asc(customer_metrics.customer_id)],
+        )
+        window_monetary = ibis.window(
+            order_by=[ibis.asc(customer_metrics.monetary), ibis.asc(customer_metrics.customer_id)],
+        )
+
+        rfm_scores = customer_metrics.mutate(
+            r_score=(ibis.ntile(10).over(window_recency)),
+            f_score=(ibis.ntile(10).over(window_frequency)),
+            m_score=(ibis.ntile(10).over(window_monetary)),
+        )
+
+        rfm_segment = (rfm_scores.r_score * 100 + rfm_scores.f_score * 10 + rfm_scores.m_score).name("rfm_segment")
path: pyretailscience/analysis/segmentation.py
user: login: mvanwyk
body: Sorry for the last minute scope change! But could you add a fm_segment column using the formula below. It's a minor change that should increase the value of the function. You'll also need to update the table in the analysis_modules.md file
```suggestion
        fm_segment = (rfm_scores.f_score * 10 + rfm_scores.m_score).name("fm_segment")
```
created_at: 2025-03-18T11:52:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -452,3 +453,106 @@ def plot(
         gu.standard_tick_styles(ax)
 
         return ax
+
+
+class RFMSegmentation:
+    """Segments customers using the RFM (Recency, Frequency, Monetary) methodology.
+
+    Customers are scored on three dimensions:
+    - Recency (R): Days since the last transaction (lower is better).
+    - Frequency (F): Number of unique transactions (higher is better).
+    - Monetary (M): Total amount spent (higher is better).
+
+    Each metric is ranked into 10 bins (0-9) using NTILE(10) where,
+    - 9 represents the best score (top 10% of customers).
+    - 0 represents the lowest score (bottom 10% of customers).
+    The RFM segment is a 3-digit number (R*100 + F*10 + M), representing customer value.
+    """
+
+    _df: pd.DataFrame | None = None
+
+    def __init__(self, df: pd.DataFrame | ibis.Table, current_date: str | None = None) -> None:
+        """Initializes the RFM segmentation process.
+
+        Args:
+            df (pd.DataFrame | ibis.Table): A DataFrame or Ibis table containing transaction data.
+                Must include the following columns:
+                - customer_id
+                - transaction_date
+                - unit_spend
+                - transaction_id
+            current_date (Optional[str]): The reference date for calculating recency (format: "YYYY-MM-DD").
path: pyretailscience/analysis/segmentation.py
user: login: mvanwyk
body: Can you make it so it's possible to also pass current_date as a `date` class object.
created_at: 2025-03-18T11:55:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -452,3 +453,106 @@ def plot(
         gu.standard_tick_styles(ax)
 
         return ax
+
+
+class RFMSegmentation:
+    """Segments customers using the RFM (Recency, Frequency, Monetary) methodology.
+
+    Customers are scored on three dimensions:
+    - Recency (R): Days since the last transaction (lower is better).
+    - Frequency (F): Number of unique transactions (higher is better).
+    - Monetary (M): Total amount spent (higher is better).
+
+    Each metric is ranked into 10 bins (0-9) using NTILE(10) where,
+    - 9 represents the best score (top 10% of customers).
+    - 0 represents the lowest score (bottom 10% of customers).
+    The RFM segment is a 3-digit number (R*100 + F*10 + M), representing customer value.
+    """
+
+    _df: pd.DataFrame | None = None
+
+    def __init__(self, df: pd.DataFrame | ibis.Table, current_date: str | None = None) -> None:
+        """Initializes the RFM segmentation process.
+
+        Args:
+            df (pd.DataFrame | ibis.Table): A DataFrame or Ibis table containing transaction data.
+                Must include the following columns:
+                - customer_id
+                - transaction_date
+                - unit_spend
+                - transaction_id
+            current_date (Optional[str]): The reference date for calculating recency (format: "YYYY-MM-DD").
+                If not provided, the current system date will be used.
+
+        Raises:
+            ValueError: If the dataframe is missing required columns.
+            TypeError: If the input data is not a pandas DataFrame or an Ibis Table.
+        """
+        cols = ColumnHelper()
+        required_cols = [
+            cols.customer_id,
+            cols.transaction_date,
+            cols.unit_spend,
+            cols.transaction_id,
+        ]
+
+        missing_cols = set(required_cols) - set(df.columns)
+        if missing_cols:
+            error_message = f"Missing required columns: {missing_cols}"
+            raise ValueError(error_message)
+        current_date = (
+            datetime.date.fromisoformat(current_date) if current_date else datetime.datetime.now(datetime.UTC).date()
+        )
+
+        self.table = self._compute_rfm(df, current_date)
path: pyretailscience/analysis/segmentation.py
user: login: mvanwyk
body: Side note: Being able to get the Ibis table object as well as the pandas dataframe of the results has been really valuable. We'll probably make sure all the analysis modules do this.
created_at: 2025-03-18T11:59:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -452,3 +453,106 @@ def plot(
         gu.standard_tick_styles(ax)
 
         return ax
+
+
+class RFMSegmentation:
+    """Segments customers using the RFM (Recency, Frequency, Monetary) methodology.
+
+    Customers are scored on three dimensions:
+    - Recency (R): Days since the last transaction (lower is better).
+    - Frequency (F): Number of unique transactions (higher is better).
+    - Monetary (M): Total amount spent (higher is better).
+
+    Each metric is ranked into 10 bins (0-9) using NTILE(10) where,
+    - 9 represents the best score (top 10% of customers).
+    - 0 represents the lowest score (bottom 10% of customers).
+    The RFM segment is a 3-digit number (R*100 + F*10 + M), representing customer value.
+    """
+
+    _df: pd.DataFrame | None = None
+
+    def __init__(self, df: pd.DataFrame | ibis.Table, current_date: str | None = None) -> None:
+        """Initializes the RFM segmentation process.
+
+        Args:
+            df (pd.DataFrame | ibis.Table): A DataFrame or Ibis table containing transaction data.
+                Must include the following columns:
+                - customer_id
+                - transaction_date
+                - unit_spend
+                - transaction_id
+            current_date (Optional[str]): The reference date for calculating recency (format: "YYYY-MM-DD").
+                If not provided, the current system date will be used.
+
+        Raises:
+            ValueError: If the dataframe is missing required columns.
+            TypeError: If the input data is not a pandas DataFrame or an Ibis Table.
+        """
+        cols = ColumnHelper()
+        required_cols = [
+            cols.customer_id,
+            cols.transaction_date,
+            cols.unit_spend,
+            cols.transaction_id,
+        ]
+
+        missing_cols = set(required_cols) - set(df.columns)
+        if missing_cols:
+            error_message = f"Missing required columns: {missing_cols}"
+            raise ValueError(error_message)
+        current_date = (
+            datetime.date.fromisoformat(current_date) if current_date else datetime.datetime.now(datetime.UTC).date()
+        )
+
+        self.table = self._compute_rfm(df, current_date)
+
+    def _compute_rfm(self, df: ibis.Table, current_date: datetime.date) -> ibis.Table:
+        """Computes the RFM metrics and segments customers accordingly.
+
+        Args:
+            df (ibis.Table): The transaction data table.
+            current_date (datetime.date): The reference date for calculating recency.
+
+        Returns:
+            ibis.Table: A table with RFM scores and segment values.
+        """
+        if isinstance(df, pd.DataFrame):
+            df = ibis.memtable(df)
+        elif not isinstance(df, ibis.Table):
+            raise TypeError("df must be either a pandas DataFrame or an Ibis Table")
+
+        cols = ColumnHelper()
+        current_date_expr = ibis.literal(current_date)
+
+        customer_metrics = df.group_by(cols.customer_id).aggregate(
+            recency_days=(current_date_expr - df[cols.transaction_date].max().cast("date")).cast("int32"),
+            frequency=df[cols.transaction_id].nunique(),
+            monetary=df[cols.unit_spend].sum(),
+        )
+
+        window_recency = ibis.window(
+            order_by=[ibis.asc(customer_metrics.recency_days), ibis.asc(customer_metrics.customer_id)],
+        )
+        window_frequency = ibis.window(
+            order_by=[ibis.asc(customer_metrics.frequency), ibis.asc(customer_metrics.customer_id)],
+        )
+        window_monetary = ibis.window(
+            order_by=[ibis.asc(customer_metrics.monetary), ibis.asc(customer_metrics.customer_id)],
+        )
+
+        rfm_scores = customer_metrics.mutate(
+            r_score=(ibis.ntile(10).over(window_recency)),
+            f_score=(ibis.ntile(10).over(window_frequency)),
+            m_score=(ibis.ntile(10).over(window_monetary)),
+        )
+
+        rfm_segment = (rfm_scores.r_score * 100 + rfm_scores.f_score * 10 + rfm_scores.m_score).name("rfm_segment")
+
+        return rfm_scores.mutate(rfm_segment=rfm_segment)
path: pyretailscience/analysis/segmentation.py
user: login: mvanwyk
body: `mutate(rfm_segment=rfm_segment)` what's this doing? Is it needed?
created_at: 2025-03-18T12:00:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -452,3 +453,106 @@ def plot(
         gu.standard_tick_styles(ax)
 
         return ax
+
+
+class RFMSegmentation:
+    """Segments customers using the RFM (Recency, Frequency, Monetary) methodology.
+
+    Customers are scored on three dimensions:
+    - Recency (R): Days since the last transaction (lower is better).
+    - Frequency (F): Number of unique transactions (higher is better).
+    - Monetary (M): Total amount spent (higher is better).
+
+    Each metric is ranked into 10 bins (0-9) using NTILE(10) where,
+    - 9 represents the best score (top 10% of customers).
+    - 0 represents the lowest score (bottom 10% of customers).
+    The RFM segment is a 3-digit number (R*100 + F*10 + M), representing customer value.
+    """
+
+    _df: pd.DataFrame | None = None
+
+    def __init__(self, df: pd.DataFrame | ibis.Table, current_date: str | None = None) -> None:
+        """Initializes the RFM segmentation process.
+
+        Args:
+            df (pd.DataFrame | ibis.Table): A DataFrame or Ibis table containing transaction data.
+                Must include the following columns:
+                - customer_id
+                - transaction_date
+                - unit_spend
+                - transaction_id
+            current_date (Optional[str]): The reference date for calculating recency (format: "YYYY-MM-DD").
+                If not provided, the current system date will be used.
+
+        Raises:
+            ValueError: If the dataframe is missing required columns.
+            TypeError: If the input data is not a pandas DataFrame or an Ibis Table.
+        """
+        cols = ColumnHelper()
+        required_cols = [
+            cols.customer_id,
+            cols.transaction_date,
+            cols.unit_spend,
+            cols.transaction_id,
+        ]
+
+        missing_cols = set(required_cols) - set(df.columns)
+        if missing_cols:
+            error_message = f"Missing required columns: {missing_cols}"
+            raise ValueError(error_message)
+        current_date = (
+            datetime.date.fromisoformat(current_date) if current_date else datetime.datetime.now(datetime.UTC).date()
+        )
+
+        self.table = self._compute_rfm(df, current_date)
+
+    def _compute_rfm(self, df: ibis.Table, current_date: datetime.date) -> ibis.Table:
+        """Computes the RFM metrics and segments customers accordingly.
+
+        Args:
+            df (ibis.Table): The transaction data table.
+            current_date (datetime.date): The reference date for calculating recency.
+
+        Returns:
+            ibis.Table: A table with RFM scores and segment values.
+        """
+        if isinstance(df, pd.DataFrame):
+            df = ibis.memtable(df)
+        elif not isinstance(df, ibis.Table):
+            raise TypeError("df must be either a pandas DataFrame or an Ibis Table")
+
+        cols = ColumnHelper()
+        current_date_expr = ibis.literal(current_date)
+
+        customer_metrics = df.group_by(cols.customer_id).aggregate(
+            recency_days=(current_date_expr - df[cols.transaction_date].max().cast("date")).cast("int32"),
+            frequency=df[cols.transaction_id].nunique(),
+            monetary=df[cols.unit_spend].sum(),
path: pyretailscience/analysis/segmentation.py
user: login: mvanwyk
body: Another scope change sorry! Having these values returned in addition to the segments would be really helpful. Can you please make that change and up the tests accordingly. 
created_at: 2025-03-18T12:02:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -452,3 +453,116 @@
         gu.standard_tick_styles(ax)
 
         return ax
+
+
+class RFMSegmentation:
+    """Segments customers using the RFM (Recency, Frequency, Monetary) methodology.
+
+    Customers are scored on three dimensions:
+    - Recency (R): Days since the last transaction (lower is better).
+    - Frequency (F): Number of unique transactions (higher is better).
+    - Monetary (M): Total amount spent (higher is better).
+
+    Each metric is ranked into 10 bins (0-9) using NTILE(10) where,
+    - 9 represents the best score (top 10% of customers).
+    - 0 represents the lowest score (bottom 10% of customers).
+    The RFM segment is a 3-digit number (R*100 + F*10 + M), representing customer value.
+    """
+
+    _df: pd.DataFrame | None = None
+
+    def __init__(self, df: pd.DataFrame | ibis.Table, current_date: str | datetime.date | None = None) -> None:
+        """Initializes the RFM segmentation process.
+
+        Args:
+            df (pd.DataFrame | ibis.Table): A DataFrame or Ibis table containing transaction data.
+                Must include the following columns:
+                - customer_id
+                - transaction_date
+                - unit_spend
+                - transaction_id
+            current_date (Optional[Union[str, datetime.date]]): The reference date for calculating recency.
+                Can be a string (format: "YYYY-MM-DD"), a date object, or None (defaults to the current system date).
+
+        Raises:
+            ValueError: If the dataframe is missing required columns.
+            TypeError: If the input data is not a pandas DataFrame or an Ibis Table.
+        """
+        cols = ColumnHelper()
+        required_cols = [
+            cols.customer_id,
+            cols.transaction_date,
+            cols.unit_spend,
+            cols.transaction_id,
+        ]
+
+        missing_cols = set(required_cols) - set(df.columns)
+        if missing_cols:
+            error_message = f"Missing required columns: {missing_cols}"
+            raise ValueError(error_message)
+
+        if isinstance(current_date, str):
+            current_date = datetime.date.fromisoformat(current_date)
+        elif current_date is None:
+            current_date = datetime.datetime.now(datetime.UTC).date()
+        elif not isinstance(current_date, datetime.date):
+            raise TypeError("current_date must be a string in 'YYYY-MM-DD' format, a datetime.date object, or None")
+
+        self.table = self._compute_rfm(df, current_date)
+
+    def _compute_rfm(self, df: ibis.Table, current_date: datetime.date) -> ibis.Table:
+        """Computes the RFM metrics and segments customers accordingly.
+
+        Args:
+            df (ibis.Table): The transaction data table.
+            current_date (datetime.date): The reference date for calculating recency.
+
+        Returns:
+            ibis.Table: A table with RFM scores and segment values.
+        """
+        if isinstance(df, pd.DataFrame):
+            df = ibis.memtable(df)
+        elif not isinstance(df, ibis.Table):
+            raise TypeError("df must be either a pandas DataFrame or an Ibis Table")
path: pyretailscience/analysis/segmentation.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add test coverage for the TypeError condition.**

This line raising `TypeError` when `df` is neither a pandas DataFrame nor an Ibis Table currently lacks test coverage. Consider adding a test that passes an invalid object to confirm the exception is raised.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 526-526: pyretailscience/analysis/segmentation.py#L526
Added line #L526 was not covered by tests

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T11:13:38Z
author_association: NONE
diff_hunk: @@ -452,3 +453,116 @@
         gu.standard_tick_styles(ax)
 
         return ax
+
+
+class RFMSegmentation:
+    """Segments customers using the RFM (Recency, Frequency, Monetary) methodology.
+
+    Customers are scored on three dimensions:
+    - Recency (R): Days since the last transaction (lower is better).
+    - Frequency (F): Number of unique transactions (higher is better).
+    - Monetary (M): Total amount spent (higher is better).
+
+    Each metric is ranked into 10 bins (0-9) using NTILE(10) where,
+    - 9 represents the best score (top 10% of customers).
+    - 0 represents the lowest score (bottom 10% of customers).
+    The RFM segment is a 3-digit number (R*100 + F*10 + M), representing customer value.
+    """
+
+    _df: pd.DataFrame | None = None
+
+    def __init__(self, df: pd.DataFrame | ibis.Table, current_date: str | datetime.date | None = None) -> None:
+        """Initializes the RFM segmentation process.
+
+        Args:
+            df (pd.DataFrame | ibis.Table): A DataFrame or Ibis table containing transaction data.
+                Must include the following columns:
+                - customer_id
+                - transaction_date
+                - unit_spend
+                - transaction_id
+            current_date (Optional[Union[str, datetime.date]]): The reference date for calculating recency.
+                Can be a string (format: "YYYY-MM-DD"), a date object, or None (defaults to the current system date).
+
+        Raises:
+            ValueError: If the dataframe is missing required columns.
+            TypeError: If the input data is not a pandas DataFrame or an Ibis Table.
+        """
+        cols = ColumnHelper()
+        required_cols = [
+            cols.customer_id,
+            cols.transaction_date,
+            cols.unit_spend,
+            cols.transaction_id,
+        ]
+
+        missing_cols = set(required_cols) - set(df.columns)
+        if missing_cols:
+            error_message = f"Missing required columns: {missing_cols}"
+            raise ValueError(error_message)
+
+        if isinstance(current_date, str):
+            current_date = datetime.date.fromisoformat(current_date)
+        elif current_date is None:
+            current_date = datetime.datetime.now(datetime.UTC).date()
+        elif not isinstance(current_date, datetime.date):
+            raise TypeError("current_date must be a string in 'YYYY-MM-DD' format, a datetime.date object, or None")
+
+        self.table = self._compute_rfm(df, current_date)
+
+    def _compute_rfm(self, df: ibis.Table, current_date: datetime.date) -> ibis.Table:
+        """Computes the RFM metrics and segments customers accordingly.
+
+        Args:
+            df (ibis.Table): The transaction data table.
+            current_date (datetime.date): The reference date for calculating recency.
+
+        Returns:
+            ibis.Table: A table with RFM scores and segment values.
+        """
+        if isinstance(df, pd.DataFrame):
+            df = ibis.memtable(df)
+        elif not isinstance(df, ibis.Table):
+            raise TypeError("df must be either a pandas DataFrame or an Ibis Table")
+
+        cols = ColumnHelper()
+        current_date_expr = ibis.literal(current_date)
+
+        customer_metrics = df.group_by(cols.customer_id).aggregate(
+            recency_days=(current_date_expr - df[cols.transaction_date].max().cast("date")).cast("int32"),
+            frequency=df[cols.transaction_id].nunique(),
+            monetary=df[cols.unit_spend].sum(),
+        )
+
+        window_recency = ibis.window(
+            order_by=[ibis.asc(customer_metrics.recency_days), ibis.asc(customer_metrics.customer_id)],
+        )
+        window_frequency = ibis.window(
+            order_by=[ibis.asc(customer_metrics.frequency), ibis.asc(customer_metrics.customer_id)],
+        )
+        window_monetary = ibis.window(
+            order_by=[ibis.asc(customer_metrics.monetary), ibis.asc(customer_metrics.customer_id)],
+        )
+
+        rfm_scores = customer_metrics.mutate(
+            r_score=(ibis.ntile(10).over(window_recency)),
+            f_score=(ibis.ntile(10).over(window_frequency)),
+            m_score=(ibis.ntile(10).over(window_monetary)),
+        )
+
+        return rfm_scores.mutate(
+            rfm_segment=(rfm_scores.r_score * 100 + rfm_scores.f_score * 10 + rfm_scores.m_score),
+            fm_segment=(rfm_scores.f_score * 10 + rfm_scores.m_score),
+        )
+
+    @property
+    def df(self) -> pd.DataFrame:
+        """Returns the dataframe with the segment names."""
+        if self._df is None:
+            self._df = self.table.execute().set_index(get_option("column.customer_id"))
+        return self._df
+
+    @property
+    def ibis_table(self) -> ibis.Table:
+        """Returns the computed Ibis table with RFM segmentation."""
+        return self.table
path: pyretailscience/analysis/segmentation.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add unit tests for the `ibis_table` property.**

Line 568 returns the `ibis_table` but the patch coverage warning suggests no direct test invokes or verifies this property. Adding a test ensures full coverage and confidence in its functionality.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 568-568: pyretailscience/analysis/segmentation.py#L568
Added line #L568 was not covered by tests

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T11:13:38Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-18T09:46:47Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request introduces RFM segmentation functionality and related documentation. A new "RFM Segmentation" section has been added to the docs, detailing the segmentation metrics and providing an example code snippet. A new `RFMSegmentation` class is added to the analysis module to calculate and return RFM scores. Additionally, comprehensive tests have been implemented to validate the functionality and ensure proper error handling.

## Changes

| File                                    | Change Summary                                                                                                                                                            |
|-----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| docs/analysis_modules.md                | Added a new "RFM Segmentation" section explaining the methodology, metrics (Recency, Frequency, Monetary), usage questions, and a Python example snippet.             |
| pyretailscience/analysis/segmentation.py| Introduced the `RFMSegmentation` class with methods (`__init__`, `_compute_rfm`, and properties `df` and `ibis_table`) for calculating RFM scores; added necessary imports and checks.  |
| tests/analysis/test_segmentation.py     | Added the `TestRFMSegmentation` class with fixtures and multiple test methods to validate correct segmentation, error handling, and calculations across scenarios. |

## Possibly related issues
- Data-Simply/pyretailscience#135: The changes in the main issue, which involve the introduction of the `RFMSegmentation` class and its functionalities, are directly related to the retrieved issue that proposes adding an RFM segmentation implementation, as both focus on the same RFM methodology and involve modifications to the `segmentation.py` file.

## Possibly related PRs
- Data-Simply/pyretailscience#89: The changes in the main PR, which introduce the `RFMSegmentation` class and its functionality, are related to the modifications in the retrieved PR that also involve segmentation logic, specifically through the use of the `ibis` framework in the `segmentation.py` module.
- Data-Simply/pyretailscience#113: The changes in the main PR, which introduce the `RFMSegmentation` class and its documentation, are directly related to the modifications in the retrieved PR that enhance documentation for segmentation modules, as both focus on RFM segmentation and its implementation.
- Data-Simply/pyretailscience#127: The changes in the main PR, which introduce the `RFMSegmentation` class and its functionality, are directly related to the modifications in the retrieved PR that involve the `segmentation` module, as both PRs deal with customer segmentation functionalities.

## Suggested labels
`enhancement`, `documentation`, `Tests`, `Review effort [1-5]: 3`

## Suggested reviewers
- mvanwyk

## Poem
> I'm a bunny with a code so bright,  
> Hopping through data day and night.  
> RFM segmentation now takes the stage,  
> With tests and docs, we set the gauge.  
> Happy hops in every byte! 🐰✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡🧪 Multi-step agentic review comment chat (experimental)</summary>
> 
> - We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
> 						- To enable this feature, set early_access to true under in the settings.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Data-Simply/pyretailscience&utm_content=140):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: codecov[bot]
created_at: 2025-03-18T09:48:24Z
body: ## [Codecov](https://app.codecov.io/gh/Data-Simply/pyretailscience/pull/140?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Data-Simply) Report
Attention: Patch coverage is `92.10526%` with `3 lines` in your changes missing coverage. Please review.

| [Files with missing lines](https://app.codecov.io/gh/Data-Simply/pyretailscience/pull/140?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Data-Simply) | Patch % | Lines |
|---|---|---|
| [pyretailscience/analysis/segmentation.py](https://app.codecov.io/gh/Data-Simply/pyretailscience/pull/140?src=pr&el=tree&filepath=pyretailscience%2Fanalysis%2Fsegmentation.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Data-Simply#diff-cHlyZXRhaWxzY2llbmNlL2FuYWx5c2lzL3NlZ21lbnRhdGlvbi5weQ==) | 92.10% | [0 Missing and 3 partials :warning: ](https://app.codecov.io/gh/Data-Simply/pyretailscience/pull/140?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Data-Simply) |

| [Files with missing lines](https://app.codecov.io/gh/Data-Simply/pyretailscience/pull/140?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Data-Simply) | Coverage Δ | |
|---|---|---|
| [pyretailscience/analysis/segmentation.py](https://app.codecov.io/gh/Data-Simply/pyretailscience/pull/140?src=pr&el=tree&filepath=pyretailscience%2Fanalysis%2Fsegmentation.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Data-Simply#diff-cHlyZXRhaWxzY2llbmNlL2FuYWx5c2lzL3NlZ21lbnRhdGlvbi5weQ==) | `76.11% <92.10%> (+0.98%)` | :arrow_up: |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/Data-Simply/pyretailscience/pull/140/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Data-Simply)

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE