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,295 @@
1
+ """Constants used throughout OpenRetailScience.
2
+
3
+ This module contains constant values that are shared across the package.
4
+ """
5
+
6
+ # Tailwind CSS color palette definitions
7
+ # https://raw.githubusercontent.com/tailwindlabs/tailwindcss/a1e74f055b13a7ef5775bdd72a77a4d397565016/src/public/colors.js
8
+ COLORS = {
9
+ "slate": {
10
+ 50: "#f8fafc",
11
+ 100: "#f1f5f9",
12
+ 200: "#e2e8f0",
13
+ 300: "#cbd5e1",
14
+ 400: "#94a3b8",
15
+ 500: "#64748b",
16
+ 600: "#475569",
17
+ 700: "#334155",
18
+ 800: "#1e293b",
19
+ 900: "#0f172a",
20
+ 950: "#020617",
21
+ },
22
+ "gray": {
23
+ 50: "#f9fafb",
24
+ 100: "#f3f4f6",
25
+ 200: "#e5e7eb",
26
+ 300: "#d1d5db",
27
+ 400: "#9ca3af",
28
+ 500: "#6b7280",
29
+ 600: "#4b5563",
30
+ 700: "#374151",
31
+ 800: "#1f2937",
32
+ 900: "#111827",
33
+ 950: "#030712",
34
+ },
35
+ "zinc": {
36
+ 50: "#fafafa",
37
+ 100: "#f4f4f5",
38
+ 200: "#e4e4e7",
39
+ 300: "#d4d4d8",
40
+ 400: "#a1a1aa",
41
+ 500: "#71717a",
42
+ 600: "#52525b",
43
+ 700: "#3f3f46",
44
+ 800: "#27272a",
45
+ 900: "#18181b",
46
+ 950: "#09090b",
47
+ },
48
+ "neutral": {
49
+ 50: "#fafafa",
50
+ 100: "#f5f5f5",
51
+ 200: "#e5e5e5",
52
+ 300: "#d4d4d4",
53
+ 400: "#a3a3a3",
54
+ 500: "#737373",
55
+ 600: "#525252",
56
+ 700: "#404040",
57
+ 800: "#262626",
58
+ 900: "#171717",
59
+ 950: "#0a0a0a",
60
+ },
61
+ "stone": {
62
+ 50: "#fafaf9",
63
+ 100: "#f5f5f4",
64
+ 200: "#e7e5e4",
65
+ 300: "#d6d3d1",
66
+ 400: "#a8a29e",
67
+ 500: "#78716c",
68
+ 600: "#57534e",
69
+ 700: "#44403c",
70
+ 800: "#292524",
71
+ 900: "#1c1917",
72
+ 950: "#0c0a09",
73
+ },
74
+ "red": {
75
+ 50: "#fef2f2",
76
+ 100: "#fee2e2",
77
+ 200: "#fecaca",
78
+ 300: "#fca5a5",
79
+ 400: "#f87171",
80
+ 500: "#ef4444",
81
+ 600: "#dc2626",
82
+ 700: "#b91c1c",
83
+ 800: "#991b1b",
84
+ 900: "#7f1d1d",
85
+ 950: "#450a0a",
86
+ },
87
+ "orange": {
88
+ 50: "#fff7ed",
89
+ 100: "#ffedd5",
90
+ 200: "#fed7aa",
91
+ 300: "#fdba74",
92
+ 400: "#fb923c",
93
+ 500: "#f97316",
94
+ 600: "#ea580c",
95
+ 700: "#c2410c",
96
+ 800: "#9a3412",
97
+ 900: "#7c2d12",
98
+ 950: "#431407",
99
+ },
100
+ "amber": {
101
+ 50: "#fffbeb",
102
+ 100: "#fef3c7",
103
+ 200: "#fde68a",
104
+ 300: "#fcd34d",
105
+ 400: "#fbbf24",
106
+ 500: "#f59e0b",
107
+ 600: "#d97706",
108
+ 700: "#b45309",
109
+ 800: "#92400e",
110
+ 900: "#78350f",
111
+ 950: "#451a03",
112
+ },
113
+ "yellow": {
114
+ 50: "#fefce8",
115
+ 100: "#fef9c3",
116
+ 200: "#fef08a",
117
+ 300: "#fde047",
118
+ 400: "#facc15",
119
+ 500: "#eab308",
120
+ 600: "#ca8a04",
121
+ 700: "#a16207",
122
+ 800: "#854d0e",
123
+ 900: "#713f12",
124
+ 950: "#422006",
125
+ },
126
+ "lime": {
127
+ 50: "#f7fee7",
128
+ 100: "#ecfccb",
129
+ 200: "#d9f99d",
130
+ 300: "#bef264",
131
+ 400: "#a3e635",
132
+ 500: "#84cc16",
133
+ 600: "#65a30d",
134
+ 700: "#4d7c0f",
135
+ 800: "#3f6212",
136
+ 900: "#365314",
137
+ 950: "#1a2e05",
138
+ },
139
+ "green": {
140
+ 50: "#f0fdf4",
141
+ 100: "#dcfce7",
142
+ 200: "#bbf7d0",
143
+ 300: "#86efac",
144
+ 400: "#4ade80",
145
+ 500: "#22c55e",
146
+ 600: "#16a34a",
147
+ 700: "#15803d",
148
+ 800: "#166534",
149
+ 900: "#14532d",
150
+ 950: "#052e16",
151
+ },
152
+ "emerald": {
153
+ 50: "#ecfdf5",
154
+ 100: "#d1fae5",
155
+ 200: "#a7f3d0",
156
+ 300: "#6ee7b7",
157
+ 400: "#34d399",
158
+ 500: "#10b981",
159
+ 600: "#059669",
160
+ 700: "#047857",
161
+ 800: "#065f46",
162
+ 900: "#064e3b",
163
+ 950: "#022c22",
164
+ },
165
+ "teal": {
166
+ 50: "#f0fdfa",
167
+ 100: "#ccfbf1",
168
+ 200: "#99f6e4",
169
+ 300: "#5eead4",
170
+ 400: "#2dd4bf",
171
+ 500: "#14b8a6",
172
+ 600: "#0d9488",
173
+ 700: "#0f766e",
174
+ 800: "#115e59",
175
+ 900: "#134e4a",
176
+ 950: "#042f2e",
177
+ },
178
+ "cyan": {
179
+ 50: "#ecfeff",
180
+ 100: "#cffafe",
181
+ 200: "#a5f3fc",
182
+ 300: "#67e8f9",
183
+ 400: "#22d3ee",
184
+ 500: "#06b6d4",
185
+ 600: "#0891b2",
186
+ 700: "#0e7490",
187
+ 800: "#155e75",
188
+ 900: "#164e63",
189
+ 950: "#083344",
190
+ },
191
+ "sky": {
192
+ 50: "#f0f9ff",
193
+ 100: "#e0f2fe",
194
+ 200: "#bae6fd",
195
+ 300: "#7dd3fc",
196
+ 400: "#38bdf8",
197
+ 500: "#0ea5e9",
198
+ 600: "#0284c7",
199
+ 700: "#0369a1",
200
+ 800: "#075985",
201
+ 900: "#0c4a6e",
202
+ 950: "#082f49",
203
+ },
204
+ "blue": {
205
+ 50: "#eff6ff",
206
+ 100: "#dbeafe",
207
+ 200: "#bfdbfe",
208
+ 300: "#93c5fd",
209
+ 400: "#60a5fa",
210
+ 500: "#3b82f6",
211
+ 600: "#2563eb",
212
+ 700: "#1d4ed8",
213
+ 800: "#1e40af",
214
+ 900: "#1e3a8a",
215
+ 950: "#172554",
216
+ },
217
+ "indigo": {
218
+ 50: "#eef2ff",
219
+ 100: "#e0e7ff",
220
+ 200: "#c7d2fe",
221
+ 300: "#a5b4fc",
222
+ 400: "#818cf8",
223
+ 500: "#6366f1",
224
+ 600: "#4f46e5",
225
+ 700: "#4338ca",
226
+ 800: "#3730a3",
227
+ 900: "#312e81",
228
+ 950: "#1e1b4b",
229
+ },
230
+ "violet": {
231
+ 50: "#f5f3ff",
232
+ 100: "#ede9fe",
233
+ 200: "#ddd6fe",
234
+ 300: "#c4b5fd",
235
+ 400: "#a78bfa",
236
+ 500: "#8b5cf6",
237
+ 600: "#7c3aed",
238
+ 700: "#6d28d9",
239
+ 800: "#5b21b6",
240
+ 900: "#4c1d95",
241
+ 950: "#2e1065",
242
+ },
243
+ "purple": {
244
+ 50: "#faf5ff",
245
+ 100: "#f3e8ff",
246
+ 200: "#e9d5ff",
247
+ 300: "#d8b4fe",
248
+ 400: "#c084fc",
249
+ 500: "#a855f7",
250
+ 600: "#9333ea",
251
+ 700: "#7e22ce",
252
+ 800: "#6b21a8",
253
+ 900: "#581c87",
254
+ 950: "#3b0764",
255
+ },
256
+ "fuchsia": {
257
+ 50: "#fdf4ff",
258
+ 100: "#fae8ff",
259
+ 200: "#f5d0fe",
260
+ 300: "#f0abfc",
261
+ 400: "#e879f9",
262
+ 500: "#d946ef",
263
+ 600: "#c026d3",
264
+ 700: "#a21caf",
265
+ 800: "#86198f",
266
+ 900: "#701a75",
267
+ 950: "#4a044e",
268
+ },
269
+ "pink": {
270
+ 50: "#fdf2f8",
271
+ 100: "#fce7f3",
272
+ 200: "#fbcfe8",
273
+ 300: "#f9a8d4",
274
+ 400: "#f472b6",
275
+ 500: "#ec4899",
276
+ 600: "#db2777",
277
+ 700: "#be185d",
278
+ 800: "#9d174d",
279
+ 900: "#831843",
280
+ 950: "#500724",
281
+ },
282
+ "rose": {
283
+ 50: "#fff1f2",
284
+ 100: "#ffe4e6",
285
+ 200: "#fecdd3",
286
+ 300: "#fda4af",
287
+ 400: "#fb7185",
288
+ 500: "#f43f5e",
289
+ 600: "#e11d48",
290
+ 700: "#be123c",
291
+ 800: "#9f1239",
292
+ 900: "#881337",
293
+ 950: "#4c0519",
294
+ },
295
+ }
File without changes
@@ -0,0 +1,29 @@
1
+ """Shared ibis expression helpers for metric calculations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ import ibis.expr.types as ir
9
+
10
+ PERCENTAGE_SCALE = 100
11
+
12
+
13
+ def ratio_metric(
14
+ numerator: ir.NumericValue,
15
+ denominator: ir.NumericValue,
16
+ scale: float = PERCENTAGE_SCALE,
17
+ ) -> ir.FloatingValue:
18
+ """Computes a scaled ratio, returning NULL on zero denominator.
19
+
20
+ Args:
21
+ numerator (ir.NumericValue): The numerator ibis expression.
22
+ denominator (ir.NumericValue): The denominator ibis expression.
23
+ scale (float, optional): Multiplicative scale factor. Defaults to 100 for percentages.
24
+
25
+ Returns:
26
+ ir.FloatingValue: The scaled ratio expression. Evaluates to NULL
27
+ when the denominator is zero (materializes as NaN in pandas).
28
+ """
29
+ return (numerator / denominator.nullif(0)) * scale
File without changes
@@ -0,0 +1,81 @@
1
+ """ACV (All Commodity Volume) metric.
2
+
3
+ ACV measures total dollar sales across all products in a set of stores,
4
+ expressed in millions ($MM).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ import ibis
12
+ from ibis import _
13
+
14
+ if TYPE_CHECKING:
15
+ import pandas as pd
16
+
17
+ from openretailscience.options import get_option
18
+ from openretailscience.utils.validation import ensure_ibis_table, validate_columns
19
+
20
+
21
+ class Acv:
22
+ """Calculates ACV (All Commodity Volume) for a set of stores.
23
+
24
+ ACV represents total dollar sales across all products, expressed in millions ($MM).
25
+ NaN values in the spend column are excluded from the sum.
26
+
27
+ Results are accessible via the `table` attribute (ibis Table) or the `df` property
28
+ (materialized pandas DataFrame).
29
+
30
+ Args:
31
+ df (pd.DataFrame | ibis.Table): Transaction data containing at least a unit_spend column.
32
+ group_col (str | list[str] | None, optional): Optional column(s) to group the ACV calculation by
33
+ (e.g., store_id). Defaults to None for total ACV.
34
+ acv_scale_factor (float, optional): Factor to scale the ACV result (default is 1,000,000 for $MM).
35
+
36
+ Raises:
37
+ TypeError: If df is not a pandas DataFrame or an Ibis Table.
38
+ ValueError: If required columns are missing from the data or if acv_scale_factor is not positive.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ df: pd.DataFrame | ibis.Table,
44
+ *,
45
+ group_col: str | list[str] | None = None,
46
+ acv_scale_factor: float = 1_000_000,
47
+ ) -> None:
48
+ """Initializes the ACV calculation."""
49
+ self._df: pd.DataFrame | None = None
50
+ self.table: ibis.Table
51
+
52
+ df = ensure_ibis_table(df)
53
+
54
+ if acv_scale_factor <= 0:
55
+ raise ValueError("acv_scale_factor must be positive.")
56
+
57
+ unit_spend_col = get_option("column.unit_spend")
58
+
59
+ if isinstance(group_col, str):
60
+ group_col = [group_col]
61
+
62
+ required_cols = [unit_spend_col]
63
+ if group_col is not None:
64
+ required_cols.extend(group_col)
65
+ validate_columns(df, required_cols)
66
+
67
+ if group_col is not None:
68
+ df = df.group_by(group_col)
69
+
70
+ self.table = df.aggregate(acv=_[unit_spend_col].sum() / acv_scale_factor)
71
+
72
+ @property
73
+ def df(self) -> pd.DataFrame:
74
+ """Returns the materialized pandas DataFrame of ACV results.
75
+
76
+ Returns:
77
+ pd.DataFrame: DataFrame with ACV values. Cached after first access.
78
+ """
79
+ if self._df is None:
80
+ self._df = self.table.execute()
81
+ return self._df
@@ -0,0 +1,118 @@
1
+ """% of Stores (Numeric Distribution) metric.
2
+
3
+ % of Stores measures the share of total stores in the dataset that sell a given product.
4
+ Every store counts equally regardless of its sales volume.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ import ibis
12
+ from ibis import _
13
+
14
+ if TYPE_CHECKING:
15
+ import pandas as pd
16
+
17
+ from openretailscience.metrics.base import ratio_metric
18
+ from openretailscience.options import ColumnHelper, get_option
19
+ from openretailscience.utils.validation import ensure_ibis_table, validate_columns
20
+
21
+ _TEMP_TOTAL_STORES = "__prs_temp_total_stores__"
22
+
23
+
24
+ class PctOfStores:
25
+ """Calculates the percentage of stores selling each product.
26
+
27
+ This is the simplest, unweighted distribution metric (numeric distribution).
28
+ It answers the question: "What fraction of stores carry this product?"
29
+
30
+ Results are accessible via the ``table`` attribute (ibis Table) or the ``df`` property
31
+ (materialized pandas DataFrame).
32
+
33
+ Args:
34
+ df (pd.DataFrame | ibis.Table): Transaction-level data containing at least
35
+ store_id and product_id columns.
36
+ product_col (str | list[str] | None, optional): Column(s) defining product granularity.
37
+ Defaults to ``get_option("column.product_id")``.
38
+ group_col (str | list[str] | None, optional): Additional grouping dimensions
39
+ (e.g., ``"category_0_name"``). Defaults to None.
40
+ within_group (bool, optional): Controls the denominator when ``group_col`` is specified.
41
+ When ``False`` (default), the percentage is relative to all stores in the dataset.
42
+ When ``True``, the percentage is relative to stores within each group independently.
43
+ Has no effect when ``group_col`` is None. Defaults to False.
44
+
45
+ Raises:
46
+ TypeError: If df is not a pandas DataFrame or an Ibis Table.
47
+ ValueError: If required columns are missing from the data, or if product_col
48
+ appears in group_col.
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ df: pd.DataFrame | ibis.Table,
54
+ *,
55
+ product_col: str | list[str] | None = None,
56
+ group_col: str | list[str] | None = None,
57
+ within_group: bool = False,
58
+ ) -> None:
59
+ """Initializes the % of Stores calculation."""
60
+ self._df: pd.DataFrame | None = None
61
+ self.table: ibis.Table
62
+
63
+ df = ensure_ibis_table(df)
64
+
65
+ store_id_col = get_option("column.store_id")
66
+
67
+ if product_col is None:
68
+ product_col = [get_option("column.product_id")]
69
+ elif isinstance(product_col, str):
70
+ product_col = [product_col]
71
+
72
+ if isinstance(group_col, str):
73
+ group_col = [group_col]
74
+
75
+ required_cols = [store_id_col, *product_col]
76
+ group_cols = list(product_col)
77
+ if group_col is not None:
78
+ overlap = set(product_col) & set(group_col)
79
+ if len(overlap) > 0:
80
+ msg = f"product_col {overlap} must not also appear in group_col"
81
+ raise ValueError(msg)
82
+ required_cols.extend(group_col)
83
+ group_cols.extend(group_col)
84
+ validate_columns(df, required_cols)
85
+
86
+ store_product = df.select([store_id_col, *group_cols]).distinct()
87
+
88
+ agg_stores_col = get_option("column.agg.store_id")
89
+ per_group = store_product.group_by(group_cols).aggregate(
90
+ **{agg_stores_col: _[store_id_col].count()},
91
+ )
92
+
93
+ use_within_group = within_group and group_col is not None
94
+ if use_within_group:
95
+ total_stores = store_product.group_by(group_col).aggregate(
96
+ **{_TEMP_TOTAL_STORES: _[store_id_col].nunique()},
97
+ )
98
+ per_group = per_group.inner_join(total_stores, group_col)
99
+ denominator = _[_TEMP_TOTAL_STORES]
100
+ else:
101
+ denominator = store_product[store_id_col].nunique()
102
+
103
+ pct_stores_col = ColumnHelper.join_options("column.agg.store_id", "column.suffix.percent")
104
+ final_cols = [*group_cols, agg_stores_col, pct_stores_col]
105
+ self.table = per_group.mutate(
106
+ **{pct_stores_col: ratio_metric(_[agg_stores_col], denominator)},
107
+ ).select(final_cols)
108
+
109
+ @property
110
+ def df(self) -> pd.DataFrame:
111
+ """Returns the materialized pandas DataFrame of % of Stores results.
112
+
113
+ Returns:
114
+ pd.DataFrame: DataFrame with % of stores values. Cached after first access.
115
+ """
116
+ if self._df is None:
117
+ self._df = self.table.execute()
118
+ return self._df