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.
- openretailscience/__init__.py +3 -0
- openretailscience/analysis/__init__.py +0 -0
- openretailscience/analysis/cohort.py +185 -0
- openretailscience/analysis/composite_rank.py +372 -0
- openretailscience/analysis/cross_shop.py +354 -0
- openretailscience/analysis/customer.py +459 -0
- openretailscience/analysis/customer_decision_hierarchy.py +359 -0
- openretailscience/analysis/gain_loss.py +340 -0
- openretailscience/analysis/haversine.py +50 -0
- openretailscience/analysis/product_association.py +418 -0
- openretailscience/analysis/revenue_tree.py +553 -0
- openretailscience/assets/fonts/Poppins-Bold.ttf +0 -0
- openretailscience/assets/fonts/Poppins-LightItalic.ttf +0 -0
- openretailscience/assets/fonts/Poppins-Medium.ttf +0 -0
- openretailscience/assets/fonts/Poppins-Regular.ttf +0 -0
- openretailscience/assets/fonts/Poppins-SemiBold.ttf +0 -0
- openretailscience/constants.py +295 -0
- openretailscience/metrics/__init__.py +0 -0
- openretailscience/metrics/base.py +29 -0
- openretailscience/metrics/distribution/__init__.py +0 -0
- openretailscience/metrics/distribution/acv.py +81 -0
- openretailscience/metrics/distribution/pct_of_stores.py +118 -0
- openretailscience/options.py +842 -0
- openretailscience/plots/__init__.py +0 -0
- openretailscience/plots/area.py +114 -0
- openretailscience/plots/bar.py +375 -0
- openretailscience/plots/broken_timeline.py +197 -0
- openretailscience/plots/cohort.py +94 -0
- openretailscience/plots/heatmap.py +134 -0
- openretailscience/plots/histogram.py +276 -0
- openretailscience/plots/index.py +505 -0
- openretailscience/plots/line.py +379 -0
- openretailscience/plots/period_on_period.py +144 -0
- openretailscience/plots/price.py +275 -0
- openretailscience/plots/scatter.py +382 -0
- openretailscience/plots/styles/__init__.py +1 -0
- openretailscience/plots/styles/colors.py +215 -0
- openretailscience/plots/styles/font_utils.py +62 -0
- openretailscience/plots/styles/graph_utils.py +834 -0
- openretailscience/plots/styles/styling_helpers.py +155 -0
- openretailscience/plots/time.py +160 -0
- openretailscience/plots/tree_diagram.py +813 -0
- openretailscience/plots/venn.py +127 -0
- openretailscience/plots/waterfall.py +217 -0
- openretailscience/plugin.py +238 -0
- openretailscience/segmentation/__init__.py +0 -0
- openretailscience/segmentation/hml.py +97 -0
- openretailscience/segmentation/nlr.py +218 -0
- openretailscience/segmentation/rfm.py +307 -0
- openretailscience/segmentation/segstats.py +1177 -0
- openretailscience/segmentation/threshold.py +165 -0
- openretailscience/utils/__init__.py +0 -0
- openretailscience/utils/date.py +226 -0
- openretailscience/utils/filter_and_label.py +52 -0
- openretailscience/utils/label.py +79 -0
- openretailscience/utils/validation.py +45 -0
- openretailscience-0.45.0.dist-info/METADATA +364 -0
- openretailscience-0.45.0.dist-info/RECORD +60 -0
- openretailscience-0.45.0.dist-info/WHEEL +4 -0
- 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
|