openretailscience 0.45.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. openretailscience/__init__.py +3 -0
  2. openretailscience/analysis/__init__.py +0 -0
  3. openretailscience/analysis/cohort.py +185 -0
  4. openretailscience/analysis/composite_rank.py +372 -0
  5. openretailscience/analysis/cross_shop.py +354 -0
  6. openretailscience/analysis/customer.py +459 -0
  7. openretailscience/analysis/customer_decision_hierarchy.py +359 -0
  8. openretailscience/analysis/gain_loss.py +340 -0
  9. openretailscience/analysis/haversine.py +50 -0
  10. openretailscience/analysis/product_association.py +418 -0
  11. openretailscience/analysis/revenue_tree.py +553 -0
  12. openretailscience/assets/fonts/Poppins-Bold.ttf +0 -0
  13. openretailscience/assets/fonts/Poppins-LightItalic.ttf +0 -0
  14. openretailscience/assets/fonts/Poppins-Medium.ttf +0 -0
  15. openretailscience/assets/fonts/Poppins-Regular.ttf +0 -0
  16. openretailscience/assets/fonts/Poppins-SemiBold.ttf +0 -0
  17. openretailscience/constants.py +295 -0
  18. openretailscience/metrics/__init__.py +0 -0
  19. openretailscience/metrics/base.py +29 -0
  20. openretailscience/metrics/distribution/__init__.py +0 -0
  21. openretailscience/metrics/distribution/acv.py +81 -0
  22. openretailscience/metrics/distribution/pct_of_stores.py +118 -0
  23. openretailscience/options.py +842 -0
  24. openretailscience/plots/__init__.py +0 -0
  25. openretailscience/plots/area.py +114 -0
  26. openretailscience/plots/bar.py +375 -0
  27. openretailscience/plots/broken_timeline.py +197 -0
  28. openretailscience/plots/cohort.py +94 -0
  29. openretailscience/plots/heatmap.py +134 -0
  30. openretailscience/plots/histogram.py +276 -0
  31. openretailscience/plots/index.py +505 -0
  32. openretailscience/plots/line.py +379 -0
  33. openretailscience/plots/period_on_period.py +144 -0
  34. openretailscience/plots/price.py +275 -0
  35. openretailscience/plots/scatter.py +382 -0
  36. openretailscience/plots/styles/__init__.py +1 -0
  37. openretailscience/plots/styles/colors.py +215 -0
  38. openretailscience/plots/styles/font_utils.py +62 -0
  39. openretailscience/plots/styles/graph_utils.py +834 -0
  40. openretailscience/plots/styles/styling_helpers.py +155 -0
  41. openretailscience/plots/time.py +160 -0
  42. openretailscience/plots/tree_diagram.py +813 -0
  43. openretailscience/plots/venn.py +127 -0
  44. openretailscience/plots/waterfall.py +217 -0
  45. openretailscience/plugin.py +238 -0
  46. openretailscience/segmentation/__init__.py +0 -0
  47. openretailscience/segmentation/hml.py +97 -0
  48. openretailscience/segmentation/nlr.py +218 -0
  49. openretailscience/segmentation/rfm.py +307 -0
  50. openretailscience/segmentation/segstats.py +1177 -0
  51. openretailscience/segmentation/threshold.py +165 -0
  52. openretailscience/utils/__init__.py +0 -0
  53. openretailscience/utils/date.py +226 -0
  54. openretailscience/utils/filter_and_label.py +52 -0
  55. openretailscience/utils/label.py +79 -0
  56. openretailscience/utils/validation.py +45 -0
  57. openretailscience-0.45.0.dist-info/METADATA +364 -0
  58. openretailscience-0.45.0.dist-info/RECORD +60 -0
  59. openretailscience-0.45.0.dist-info/WHEEL +4 -0
  60. openretailscience-0.45.0.dist-info/licenses/LICENSE +93 -0
@@ -0,0 +1,418 @@
1
+ """Market Basket Analysis and Product Association Rules for Retail Optimization.
2
+
3
+ ## Business Context
4
+
5
+ Product association analysis (market basket analysis) uncovers the hidden relationships
6
+ in customer purchasing behavior. This intelligence transforms how retailers approach
7
+ merchandising, marketing, and operations by revealing which products naturally sell
8
+ together and why.
9
+
10
+ ## The Business Problem
11
+
12
+ Retailers lose revenue from missed cross-selling opportunities and poor product
13
+ placement. Without understanding product associations, stores might:
14
+ - Place complementary items in different aisles, reducing impulse purchases
15
+ - Miss bundling opportunities that could increase average transaction value
16
+ - Stock-out on associated items when promoting a product
17
+ - Fail to identify new product opportunities based on basket gaps
18
+
19
+ ## Real-World Applications
20
+
21
+ 1. **Strategic Merchandising**
22
+ - Place chips near beer when data shows strong association
23
+ - Position phone cases near phones based on attachment rates
24
+ - Create end-cap displays with products that sell together
25
+ - Optimize shelf space allocation using association strength
26
+
27
+ 2. **Dynamic Bundle Pricing**
28
+ - Create "Breakfast bundle": Coffee + Pastry when uplift shows synergy
29
+ - Design seasonal bundles based on historical associations
30
+ - Price bundles to incentivize larger baskets while maintaining margins
31
+ - Test bundle combinations using confidence metrics
32
+
33
+ 3. **Personalized Recommendations**
34
+ - Power "Customers who bought X also bought Y" suggestions
35
+ - Enhance cart abandonment recovery with associated items
36
+ - Design email campaigns based on previous purchase associations
37
+ - Improve search results by showing associated products
38
+
39
+ 4. **Inventory Optimization**
40
+ - Stock pasta sauce when pasta is promoted (if association exists)
41
+ - Prepare battery inventory when toys are featured
42
+ - Coordinate supply chain for products that sell together
43
+ - Reduce stockouts by understanding product relationships
44
+
45
+ 5. **New Product Placement**
46
+ - Place new organic items near existing organic purchases
47
+ - Position private label next to associated national brands
48
+ - Test new products within high-association categories
49
+ - Leverage existing associations to drive trial
50
+
51
+ ## Key Metrics Explained
52
+
53
+ ### Support (Frequency)
54
+ The proportion of all transactions containing both products. Higher support
55
+ indicates a more common pairing. Use this to identify:
56
+ - Core product relationships for everyday decisions
57
+ - Sufficient sample size for confident conclusions
58
+ - Opportunities worth marketing investment
59
+
60
+ ### Confidence (Conditional Probability)
61
+ The probability of buying product B given product A was purchased. This answers
62
+ "If a customer buys A, how likely are they to buy B?" Use this for:
63
+ - Recommendation engine rules
64
+ - Promotional targeting decisions
65
+ - Cross-sell prioritization
66
+
67
+ ### Uplift/Lift (Synergy Indicator)
68
+ Measures how much more likely products are bought together than would be expected
69
+ by chance. Uplift = Observed probability / Expected probability. Interpretation:
70
+ - Uplift > 1: Products have positive association (sell better together)
71
+ - Uplift = 1: Products are independent (no relationship)
72
+ - Uplift < 1: Products have negative association (rarely bought together)
73
+
74
+ Higher uplift values indicate stronger synergies worth exploiting.
75
+
76
+ ## Actionable Decision Framework
77
+
78
+ Retailers should consider multiple metrics together:
79
+
80
+ **High Support + High Confidence**
81
+ - Strong, frequent relationship
82
+ - Priority for permanent merchandising changes
83
+ - Core bundle candidates
84
+
85
+ **Low Support + High Confidence**
86
+ - Niche but reliable relationship
87
+ - Targeted marketing opportunities
88
+ - Specialized customer segments
89
+
90
+ **High Support + Low Confidence**
91
+ - Common but weak relationship
92
+ - Test before major changes
93
+ - Monitor for shifts over time
94
+
95
+ **High Uplift (regardless of support)**
96
+ - Strong synergy exists
97
+ - Test for merchandising opportunities
98
+ - Consider for promotional strategies
99
+
100
+ ## Implementation Considerations
101
+
102
+ - Validate associations across time periods before major changes
103
+ - Consider seasonality in association patterns
104
+ - Test recommendations with A/B experiments
105
+ - Monitor associations as product mix evolves
106
+ - Account for external factors (promotions, events) affecting associations
107
+ """
108
+
109
+ import ibis
110
+ import pandas as pd
111
+ from ibis import _
112
+
113
+ from openretailscience.options import get_option
114
+
115
+
116
+ class ProductAssociation:
117
+ """A class for generating and analyzing product association rules.
118
+
119
+ This class calculates association rules between products based on transaction data,
120
+ helping to identify patterns in customer purchasing behavior.
121
+
122
+ Args:
123
+ df (pandas.DataFrame): The input DataFrame containing transaction data.
124
+ value_col (str): The name of the column in the input DataFrame that contains
125
+ the product identifiers.
126
+ group_col (str, optional): The name of the column that identifies unique
127
+ transactions or customers. Defaults to option column.column_id.
128
+ target_item (str | float | list[str | float] | None, optional): A specific
129
+ product or list of products to focus the association analysis on.
130
+ If None, associations for all products are calculated. Defaults to None.
131
+
132
+ Attributes:
133
+ df (pandas.DataFrame): A DataFrame containing the calculated association
134
+ rules and their metrics.
135
+
136
+ Example:
137
+ >>> import pandas as pd
138
+ >>> transaction_df = pd.DataFrame({
139
+ ... 'customer_id': [1, 1, 2, 2, 3],
140
+ ... 'product_id': ['A', 'B', 'B', 'C', 'A']
141
+ ... })
142
+ >>> pa = ProductAssociation(df=transaction_df, value_col='product_id', group_col='customer_id')
143
+ >>> print(pa.df) # View the calculated association rules
144
+
145
+ Note:
146
+ The resulting DataFrame (pa.df) contains the following columns:
147
+ - product_1, product_2: The pair of products for which the association is calculated.
148
+ - occurrences_1, occurrences_2: The number of transactions containing each product.
149
+ - cooccurrences: The number of transactions containing both products.
150
+ - support: The proportion of transactions containing both products.
151
+ - confidence: The probability of buying product_2 given that product_1 was bought.
152
+ - uplift: The ratio of the observed support to the expected support if the products were independent.
153
+ """
154
+
155
+ def __init__(
156
+ self,
157
+ df: pd.DataFrame | ibis.Table,
158
+ value_col: str,
159
+ group_col: str | None = None,
160
+ target_item: str | None = None,
161
+ min_occurrences: int = 1,
162
+ min_cooccurrences: int = 1,
163
+ min_support: float = 0.0,
164
+ min_confidence: float = 0.0,
165
+ min_uplift: float = 0.0,
166
+ ) -> None:
167
+ """Initialize the ProductAssociation object.
168
+
169
+ Args:
170
+ df (pd.DataFrame | ibis.Table) : The input DataFrame or ibis Table containing transaction data.
171
+ value_col (str): The name of the column in the input DataFrame that contains the product identifiers.
172
+ group_col (str, optional): The name of the column that identifies unique transactions or customers. Defaults
173
+ to option column.unit_spend.
174
+ target_item (str | float | list[str | float] | None, optional): A specific product
175
+ or list of products to focus the association analysis on. If None,
176
+ associations for all products are calculated. Defaults to None.
177
+ min_occurrences (int, optional): The minimum number of occurrences required for each product in the
178
+ association analysis. Defaults to 1. Must be at least 1.
179
+ min_cooccurrences (int, optional): The minimum number of co-occurrences required for the product pairs in
180
+ the association analysis. Defaults to 1. Must be at least 1.
181
+ min_support (float, optional): The minimum support value required for the association rules. Defaults to
182
+ 0.0. Must be between 0 and 1.
183
+ min_confidence (float, optional): The minimum confidence value required for the association rules. Defaults
184
+ to 0.0. Must be between 0 and 1.
185
+ min_uplift (float, optional): The minimum uplift value required for the association rules. Defaults to 0.0.
186
+ Must be greater or equal to 0.
187
+
188
+ Raises:
189
+ ValueError: If the number of combinations is not 2 or 3, or if any of the minimum values are invalid.
190
+ ValueError: If the minimum support, confidence, or uplift values are outside the valid range.
191
+ ValueError: If the minimum occurrences or cooccurrences are less than 1.
192
+ ValueError: If the input DataFrame does not contain the required columns or if they have null values.
193
+ """
194
+ self._df: pd.DataFrame | None = None
195
+ group_col = group_col or get_option("column.customer_id")
196
+ required_cols = [group_col, value_col]
197
+ missing_cols = set(required_cols) - set(df.columns)
198
+ if len(missing_cols) > 0:
199
+ msg = f"The following columns are required but missing: {missing_cols}"
200
+ raise ValueError(msg)
201
+
202
+ self.table = self._calc_association(
203
+ df=df,
204
+ value_col=value_col,
205
+ group_col=group_col,
206
+ target_item=target_item,
207
+ min_occurrences=min_occurrences,
208
+ min_cooccurrences=min_cooccurrences,
209
+ min_support=min_support,
210
+ min_confidence=min_confidence,
211
+ min_uplift=min_uplift,
212
+ )
213
+
214
+ @staticmethod
215
+ def _validate_minimum_values(
216
+ min_occurrences: int,
217
+ min_cooccurrences: int,
218
+ min_support: float,
219
+ min_confidence: float,
220
+ min_uplift: float,
221
+ ) -> None:
222
+ """Validate minimum value parameters.
223
+
224
+ Args:
225
+ min_occurrences (int): The minimum number of occurrences required for each product.
226
+ min_cooccurrences (int): The minimum number of co-occurrences required for product pairs.
227
+ min_support (float): The minimum support value required for association rules.
228
+ min_confidence (float): The minimum confidence value required for association rules.
229
+ min_uplift (float): The minimum uplift value required for association rules.
230
+
231
+ Raises:
232
+ ValueError: If any parameter is outside the valid range.
233
+ """
234
+ if min_occurrences < 1:
235
+ raise ValueError("Minimum occurrences must be at least 1")
236
+ if min_cooccurrences < 1:
237
+ raise ValueError("Minimum cooccurrences must be at least 1")
238
+ if min_support < 0.0 or min_support > 1.0:
239
+ raise ValueError("Minimum support must be between 0 and 1")
240
+ if min_confidence < 0.0 or min_confidence > 1.0:
241
+ raise ValueError("Minimum confidence must be between 0 and 1")
242
+ if min_uplift < 0.0:
243
+ raise ValueError("Minimum uplift must be greater or equal to 0")
244
+
245
+ @staticmethod
246
+ def _calc_association(
247
+ df: pd.DataFrame | ibis.Table,
248
+ value_col: str,
249
+ group_col: str = get_option("column.customer_id"),
250
+ target_item: str | float | list[str | float] | None = None,
251
+ min_occurrences: int = 1,
252
+ min_cooccurrences: int = 1,
253
+ min_support: float = 0.0,
254
+ min_confidence: float = 0.0,
255
+ min_uplift: float = 0.0,
256
+ ) -> pd.DataFrame:
257
+ """Calculate product association rules based on transaction data.
258
+
259
+ This method calculates association rules between products based on transaction data,
260
+ helping to identify patterns in customer purchasing behavior.
261
+
262
+ Args:
263
+ df (pd.DataFrame | ibis.Table) : The input DataFrame or ibis Table containing transaction data.
264
+ value_col (str): The name of the column in the input DataFrame that contains the product identifiers.
265
+ group_col (str, optional): The name of the column that identifies unique transactions or customers. Defaults
266
+ to option column.unit_spend.
267
+ target_item (str | float | list[str | float] | None, optional): A specific product
268
+ or list of products to focus the association analysis on. If None,
269
+ associations for all products are calculated. Defaults to None.
270
+ min_occurrences (int, optional): The minimum number of occurrences required for each product in the
271
+ association analysis. Defaults to 1. Must be at least 1.
272
+ min_cooccurrences (int, optional): The minimum number of co-occurrences required for the product pairs in
273
+ the association analysis. Defaults to 1. Must be at least 1.
274
+ min_support (float, optional): The minimum support value required for the association rules. Defaults to
275
+ 0.0. Must be between 0 and 1.
276
+ min_confidence (float, optional): The minimum confidence value required for the association rules. Defaults
277
+ to 0.0. Must be between 0 and 1.
278
+ min_uplift (float, optional): The minimum uplift value required for the association rules. Defaults to 0.0.
279
+ Must be greater or equal to 0.
280
+
281
+ Returns:
282
+ pandas.DataFrame: A DataFrame containing the calculated association rules and their metrics.
283
+
284
+ Raises:
285
+ ValueError: If the number of combinations is not 2 or 3, or if any of the minimum values are invalid.
286
+ ValueError: If the minimum support, confidence, or uplift values are outside the valid range.
287
+ ValueError: If the minimum occurrences or cooccurrences are less than 1.
288
+
289
+ Note:
290
+ The resulting DataFrame contains the following columns:
291
+ - product_1, product_2: The pair of products for which the association is calculated.
292
+ - occurrences_1, occurrences_2: The number of transactions containing each product.
293
+ - cooccurrences: The number of transactions containing both products.
294
+ - support: The proportion of transactions containing both products.
295
+ - confidence: The probability of buying product_2 given that product_1 was bought.
296
+ - uplift: The ratio of the observed support to the expected support if the products were independent.
297
+ """
298
+ ProductAssociation._validate_minimum_values(
299
+ min_occurrences=min_occurrences,
300
+ min_cooccurrences=min_cooccurrences,
301
+ min_support=min_support,
302
+ min_confidence=min_confidence,
303
+ min_uplift=min_uplift,
304
+ )
305
+
306
+ # Normalize target_item to a list for consistent processing
307
+ if target_item is not None:
308
+ if not isinstance(target_item, list):
309
+ target_item = [target_item]
310
+
311
+ # Validate that all items in target_item are of supported types
312
+ for item in target_item:
313
+ if not isinstance(item, str | float):
314
+ msg = f"target_item must contain only str or float values. Got {type(item)}"
315
+ raise TypeError(msg)
316
+
317
+ # Ensure target_item is not empty
318
+ if len(target_item) == 0:
319
+ raise ValueError("target_item cannot be an empty list")
320
+
321
+ if isinstance(df, pd.DataFrame):
322
+ df = ibis.memtable(df)
323
+
324
+ unique_transactions = df.select(_[group_col], _[value_col]).distinct()
325
+ total_transactions = unique_transactions.alias("t")[group_col].nunique().name("total_count")
326
+
327
+ product_occurrences = (
328
+ unique_transactions.group_by(value_col)
329
+ .aggregate(occurrences=_[group_col].nunique())
330
+ .mutate(occurrence_probability=_.occurrences / total_transactions)
331
+ .filter(_.occurrences >= min_occurrences)
332
+ )
333
+
334
+ left_table = unique_transactions.rename({"item_1": value_col})
335
+ right_table = unique_transactions.rename({"item_2": value_col})
336
+
337
+ join_logic = [left_table[group_col] == right_table[group_col]]
338
+ if target_item is None:
339
+ join_logic.append(left_table.item_1 < right_table.item_2)
340
+ else:
341
+ join_logic.extend(
342
+ [
343
+ left_table.item_1 != right_table.item_2,
344
+ left_table.item_1.isin(target_item),
345
+ ],
346
+ )
347
+ merged_df = left_table.join(right_table, predicates=join_logic)
348
+
349
+ cooccurrences = (
350
+ merged_df.group_by(["item_1", "item_2"])
351
+ .aggregate(cooccurrences=merged_df[group_col].nunique())
352
+ .mutate(support=_.cooccurrences / total_transactions)
353
+ .filter((_.cooccurrences >= min_cooccurrences) & (_.support >= min_support))
354
+ )
355
+
356
+ product_occurrences_1_rename = product_occurrences.rename(
357
+ {"item_1": value_col, "occurrences_1": "occurrences", "prob_1": "occurrence_probability"},
358
+ )
359
+ product_occurrences_2_rename = product_occurrences.rename(
360
+ {"item_2": value_col, "occurrences_2": "occurrences", "prob_2": "occurrence_probability"},
361
+ )
362
+
363
+ result = (
364
+ cooccurrences.join(product_occurrences_1_rename, "item_1")
365
+ .join(product_occurrences_2_rename, "item_2")
366
+ .mutate(
367
+ confidence=_.cooccurrences / _.occurrences_1,
368
+ uplift=_.support / (_.prob_1 * _.prob_2),
369
+ )
370
+ .filter(_.uplift >= min_uplift)
371
+ )
372
+
373
+ if target_item is None:
374
+ col_order = [
375
+ "item_1",
376
+ "item_2",
377
+ "occurrences_1",
378
+ "occurrences_2",
379
+ "cooccurrences",
380
+ "support",
381
+ "confidence",
382
+ "uplift",
383
+ ]
384
+ inverse_pairs = result.mutate(
385
+ item_1=result.item_2,
386
+ item_2=result.item_1,
387
+ occurrences_1=result.occurrences_2,
388
+ occurrences_2=result.occurrences_1,
389
+ prob_1=result.prob_2,
390
+ prob_2=result.prob_1,
391
+ confidence=result.cooccurrences / result.occurrences_2,
392
+ )
393
+ result = result[col_order].union(inverse_pairs[col_order])
394
+
395
+ result = (
396
+ result.filter(result.confidence >= min_confidence)
397
+ .order_by(["item_1", "item_2"])
398
+ .rename({f"{value_col}_1": "item_1", f"{value_col}_2": "item_2"})
399
+ )
400
+ return result[
401
+ [
402
+ f"{value_col}_1",
403
+ f"{value_col}_2",
404
+ "occurrences_1",
405
+ "occurrences_2",
406
+ "cooccurrences",
407
+ "support",
408
+ "confidence",
409
+ "uplift",
410
+ ]
411
+ ]
412
+
413
+ @property
414
+ def df(self) -> pd.DataFrame:
415
+ """Returns the executed DataFrame."""
416
+ if self._df is None:
417
+ self._df = self.table.execute().reset_index(drop=True)
418
+ return self._df