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,155 @@
|
|
|
1
|
+
"""PlotStyler class that applies styling using the options system."""
|
|
2
|
+
|
|
3
|
+
from matplotlib.axes import Axes
|
|
4
|
+
from matplotlib.text import Text
|
|
5
|
+
|
|
6
|
+
from openretailscience.options import PlotStyleHelper
|
|
7
|
+
from openretailscience.plots.styles.font_utils import get_font_properties
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PlotStyler:
|
|
11
|
+
"""Helper class for applying all styling using the options system."""
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
"""Initialize the PlotStyler."""
|
|
15
|
+
self._style = PlotStyleHelper()
|
|
16
|
+
|
|
17
|
+
def apply_base_styling(self, ax: Axes) -> None:
|
|
18
|
+
"""Apply base plot styling (spines, grid, background) using options."""
|
|
19
|
+
ax.set_facecolor(self._style.background_color)
|
|
20
|
+
ax.set_axisbelow(True)
|
|
21
|
+
|
|
22
|
+
# Configure spines based on options
|
|
23
|
+
ax.spines["top"].set_visible(self._style.show_top_spine)
|
|
24
|
+
ax.spines["right"].set_visible(self._style.show_right_spine)
|
|
25
|
+
ax.spines["bottom"].set_visible(self._style.show_bottom_spine)
|
|
26
|
+
ax.spines["left"].set_visible(self._style.show_left_spine)
|
|
27
|
+
|
|
28
|
+
# Configure grid based on options
|
|
29
|
+
ax.grid(which="major", axis="x", color=self._style.grid_color, alpha=self._style.grid_alpha, zorder=1)
|
|
30
|
+
ax.grid(which="major", axis="y", color=self._style.grid_color, alpha=self._style.grid_alpha, zorder=1)
|
|
31
|
+
|
|
32
|
+
def apply_title(self, ax: Axes, title: str, pad: int | None = None) -> None:
|
|
33
|
+
"""Apply title styling using options."""
|
|
34
|
+
effective_pad = pad if pad is not None else self._style.title_pad
|
|
35
|
+
|
|
36
|
+
ax.set_title(
|
|
37
|
+
title,
|
|
38
|
+
fontproperties=get_font_properties(self._style.title_font),
|
|
39
|
+
fontsize=self._style.title_size,
|
|
40
|
+
pad=effective_pad,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def apply_label(self, ax: Axes, label: str, axis: str, pad: int | None = None) -> None:
|
|
44
|
+
"""Apply axis label styling using options."""
|
|
45
|
+
# Use appropriate default padding based on axis
|
|
46
|
+
if pad is None:
|
|
47
|
+
pad = self._style.x_label_pad if axis == "x" else self._style.y_label_pad
|
|
48
|
+
|
|
49
|
+
font_props = get_font_properties(self._style.label_font)
|
|
50
|
+
|
|
51
|
+
axis_fn = ax.set_xlabel if axis == "x" else ax.set_ylabel
|
|
52
|
+
axis_fn(label, fontproperties=font_props, fontsize=self._style.label_size, labelpad=pad)
|
|
53
|
+
|
|
54
|
+
def apply_ticks(self, ax: Axes) -> None:
|
|
55
|
+
"""Apply tick styling using options."""
|
|
56
|
+
ax.tick_params(axis="both", which="both", labelsize=self._style.tick_size)
|
|
57
|
+
|
|
58
|
+
tick_font_props = get_font_properties(self._style.tick_font)
|
|
59
|
+
for tick in [
|
|
60
|
+
*ax.xaxis.get_major_ticks(),
|
|
61
|
+
*ax.xaxis.get_minor_ticks(),
|
|
62
|
+
*ax.yaxis.get_major_ticks(),
|
|
63
|
+
*ax.yaxis.get_minor_ticks(),
|
|
64
|
+
]:
|
|
65
|
+
tick.label1.set_fontproperties(tick_font_props)
|
|
66
|
+
tick.label2.set_fontproperties(tick_font_props)
|
|
67
|
+
|
|
68
|
+
def apply_source_text(
|
|
69
|
+
self,
|
|
70
|
+
ax: Axes,
|
|
71
|
+
text: str,
|
|
72
|
+
font_size: float | None = None,
|
|
73
|
+
vertical_padding: float = 2,
|
|
74
|
+
is_venn_diagram: bool = False,
|
|
75
|
+
**kwargs: object,
|
|
76
|
+
) -> Text:
|
|
77
|
+
"""Apply source text styling using options.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
ax (Axes): The graph to add the source text to.
|
|
81
|
+
text (str): The source text.
|
|
82
|
+
font_size (float, optional): The font size of the source text. If None, uses options default.
|
|
83
|
+
vertical_padding (float, optional): The padding in ems below the x-axis label. Defaults to 2.
|
|
84
|
+
is_venn_diagram (bool, optional): Flag to indicate if the diagram is a Venn diagram.
|
|
85
|
+
If True, `x_norm` and `y_norm` will be set to fixed values. Defaults to False.
|
|
86
|
+
**kwargs: Additional keyword arguments for positioning (x, y coordinates)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Text: The source text object.
|
|
90
|
+
"""
|
|
91
|
+
effective_font_size = font_size or self._style.source_size
|
|
92
|
+
|
|
93
|
+
# Calculate position if not provided in kwargs
|
|
94
|
+
if "x" not in kwargs or "y" not in kwargs:
|
|
95
|
+
ax.figure.canvas.draw()
|
|
96
|
+
|
|
97
|
+
if is_venn_diagram:
|
|
98
|
+
x_norm, y_norm = 0.01, 0.02
|
|
99
|
+
else:
|
|
100
|
+
# Get y coordinate of the text
|
|
101
|
+
xlabel_box = ax.xaxis.label.get_window_extent(renderer=ax.figure.canvas.get_renderer())
|
|
102
|
+
|
|
103
|
+
top_of_label_px = xlabel_box.y0
|
|
104
|
+
padding_px = vertical_padding * effective_font_size
|
|
105
|
+
y_disp = top_of_label_px - padding_px - (xlabel_box.height)
|
|
106
|
+
|
|
107
|
+
# Convert display coordinates to normalized figure coordinates
|
|
108
|
+
y_norm = y_disp / ax.figure.bbox.height
|
|
109
|
+
|
|
110
|
+
# Get x coordinate of the text
|
|
111
|
+
ylabel_box = ax.yaxis.label.get_window_extent(renderer=ax.figure.canvas.get_renderer())
|
|
112
|
+
title_box = ax.title.get_window_extent(renderer=ax.figure.canvas.get_renderer())
|
|
113
|
+
min_x0 = min(ylabel_box.x0, title_box.x0)
|
|
114
|
+
x_norm = ax.figure.transFigure.inverted().transform((min_x0, 0))[0]
|
|
115
|
+
|
|
116
|
+
# Use calculated positions if not provided
|
|
117
|
+
x_pos = kwargs.get("x", x_norm)
|
|
118
|
+
y_pos = kwargs.get("y", y_norm)
|
|
119
|
+
else:
|
|
120
|
+
x_pos = kwargs.get("x", 0.01)
|
|
121
|
+
y_pos = kwargs.get("y", 0.02)
|
|
122
|
+
|
|
123
|
+
return ax.figure.text(
|
|
124
|
+
x_pos,
|
|
125
|
+
y_pos,
|
|
126
|
+
text,
|
|
127
|
+
ha="left",
|
|
128
|
+
va="bottom",
|
|
129
|
+
transform=ax.figure.transFigure,
|
|
130
|
+
fontsize=effective_font_size,
|
|
131
|
+
fontproperties=get_font_properties(self._style.source_font),
|
|
132
|
+
color="dimgray",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def apply_legend(self, ax: Axes, title: str | None = None, outside: bool = False) -> None:
|
|
136
|
+
"""Apply legend styling using options."""
|
|
137
|
+
if outside:
|
|
138
|
+
legend = ax.legend(
|
|
139
|
+
bbox_to_anchor=self._style.legend_bbox_to_anchor,
|
|
140
|
+
loc=self._style.legend_loc,
|
|
141
|
+
frameon=False,
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
legend = ax.legend(frameon=False)
|
|
145
|
+
|
|
146
|
+
if title:
|
|
147
|
+
legend.set_title(title)
|
|
148
|
+
legend.get_title().set_fontproperties(get_font_properties(self._style.label_font))
|
|
149
|
+
legend.get_title().set_fontsize(self._style.label_size)
|
|
150
|
+
|
|
151
|
+
# Apply styling to legend text
|
|
152
|
+
legend_font_props = get_font_properties(self._style.label_font)
|
|
153
|
+
for text in legend.get_texts():
|
|
154
|
+
text.set_fontproperties(legend_font_props)
|
|
155
|
+
text.set_fontsize(self._style.label_size - 1)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""This module provides functionality for creating timeline plots.
|
|
2
|
+
|
|
3
|
+
Which are essential for visualizing transactional data over time.
|
|
4
|
+
By aggregating data by specified periods (e.g., daily, weekly, monthly), timeline plots help to identify
|
|
5
|
+
trends, seasonal patterns, and performance variations across different timeframes. These plots are valuable tools for
|
|
6
|
+
retail analysis, sales tracking, and customer behavior insights.
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **Timeline Plot Creation**: Plot a value column (e.g., sales, transactions) over time,
|
|
11
|
+
aggregated by a specific period (e.g., daily, weekly).
|
|
12
|
+
- **Customizable Aggregation**: Supports different aggregation functions (e.g., sum, average)
|
|
13
|
+
to compute the value column's metrics.
|
|
14
|
+
- **Grouping by Categories**: Optionally group data by a specific category (e.g., product,
|
|
15
|
+
region, store) and compare performance over time.
|
|
16
|
+
- **Time Period Handling**: The `period` parameter allows data aggregation by different time
|
|
17
|
+
periods, such as days, weeks, or months.
|
|
18
|
+
- **Graph Styling**: Customize the appearance of the plot with options to adjust titles, axis
|
|
19
|
+
labels, legend placement, and more.
|
|
20
|
+
- **Color Mapping**: Use linear color gradients for category-based groupings to visually
|
|
21
|
+
differentiate between groups in the timeline.
|
|
22
|
+
|
|
23
|
+
### Use Cases
|
|
24
|
+
|
|
25
|
+
- **Sales and Revenue Analysis**: Track sales performance over time, either as a total or by
|
|
26
|
+
group (e.g., product category or store).
|
|
27
|
+
- **Seasonal Trend Analysis**: Visualize how sales or transaction values fluctuate across
|
|
28
|
+
different periods, helping to identify seasonal trends or promotional impacts.
|
|
29
|
+
- **Customer Behavior Tracking**: Examine changes in customer behavior (e.g., purchase
|
|
30
|
+
frequency, average transaction value) over time.
|
|
31
|
+
- **Comparative Performance**: Compare multiple categories (e.g., different products or regions)
|
|
32
|
+
on the same timeline to evaluate relative performance.
|
|
33
|
+
|
|
34
|
+
### Limitations and Handling of Data
|
|
35
|
+
|
|
36
|
+
- **Time Period Grouping**: Data is aggregated by a time period defined by the `period`
|
|
37
|
+
argument, which can be adjusted to daily, weekly, monthly, etc.
|
|
38
|
+
- **Grouping by Categories**: If `group_col` is specified, the plot will display performance
|
|
39
|
+
across different categories, with color differentiation for each group.
|
|
40
|
+
- **Flexible Aggregation**: The aggregation function (e.g., sum, average) can be customized to
|
|
41
|
+
calculate the desired value for each period.
|
|
42
|
+
|
|
43
|
+
### Functionality Details
|
|
44
|
+
|
|
45
|
+
- **plot()**: Generates a timeline plot of a specified value column over time, with
|
|
46
|
+
customization options for grouping, aggregation, and styling.
|
|
47
|
+
- **Helper functions**: Utilizes utility functions from the `openretailscience` package to handle
|
|
48
|
+
styling, formatting, and other plot adjustments.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
import numpy as np
|
|
52
|
+
import pandas as pd
|
|
53
|
+
from matplotlib.axes import Axes, SubplotBase
|
|
54
|
+
from pandas.tseries.offsets import BaseOffset
|
|
55
|
+
|
|
56
|
+
import openretailscience.plots.styles.graph_utils as gu
|
|
57
|
+
from openretailscience.options import get_option
|
|
58
|
+
from openretailscience.plots.styles.colors import get_linear_cmap, get_named_color
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def plot(
|
|
62
|
+
df: pd.DataFrame,
|
|
63
|
+
value_col: str,
|
|
64
|
+
period: str | BaseOffset = "D",
|
|
65
|
+
agg_func: str = "sum",
|
|
66
|
+
group_col: str | None = None,
|
|
67
|
+
title: str | None = None,
|
|
68
|
+
x_label: str | None = None,
|
|
69
|
+
y_label: str | None = None,
|
|
70
|
+
legend_title: str | None = None,
|
|
71
|
+
ax: Axes | None = None,
|
|
72
|
+
source_text: str | None = None,
|
|
73
|
+
move_legend_outside: bool = False,
|
|
74
|
+
**kwargs: dict[str, any],
|
|
75
|
+
) -> SubplotBase:
|
|
76
|
+
"""Plots the value_col over time.
|
|
77
|
+
|
|
78
|
+
Timeline plots are a fundamental tool for interpreting transactional data within a temporal context. By presenting
|
|
79
|
+
data in a chronological sequence, these visualizations reveal patterns and trends that might otherwise remain hidden
|
|
80
|
+
in raw numbers, making them essential for both historical analysis and forward-looking insights. They are
|
|
81
|
+
particularly useful for:
|
|
82
|
+
|
|
83
|
+
- Tracking sales performance across different periods (e.g., daily, weekly, monthly)
|
|
84
|
+
- Identifying seasonal patterns or promotional impacts on sales
|
|
85
|
+
- Comparing the performance of different product categories or store locations over time
|
|
86
|
+
- Visualizing customer behavior trends, such as purchase frequency or average transaction value
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
df (pd.DataFrame): The dataframe to plot.
|
|
90
|
+
value_col (str): The column to plot.
|
|
91
|
+
period (str | BaseOffset): The period to group the data by.
|
|
92
|
+
agg_func (str, optional): The aggregation function to apply to the value_col. Defaults to "sum".
|
|
93
|
+
group_col (str, optional): The column to group the data by. Defaults to None.
|
|
94
|
+
title (str, optional): The title of the plot. Defaults to None. When None the title is set to
|
|
95
|
+
`f"{value_col.title()} by {group_col.title()}"`
|
|
96
|
+
x_label (str, optional): The x-axis label. Defaults to None. When None the x-axis label is set to blank
|
|
97
|
+
y_label (str, optional): The y-axis label. Defaults to None. When None the y-axis label is set to the title
|
|
98
|
+
case of `value_col`
|
|
99
|
+
legend_title (str, optional): The title of the legend. Defaults to None. When None the legend title is set to
|
|
100
|
+
the title case of `group_col`
|
|
101
|
+
ax (Axes, optional): The matplotlib axes object to plot on. Defaults to None.
|
|
102
|
+
source_text (str, optional): The source text to add to the plot. Defaults to None.
|
|
103
|
+
move_legend_outside (bool, optional): Whether to move the legend outside the plot. Defaults to True.
|
|
104
|
+
**kwargs: Additional keyword arguments to pass to the Pandas plot function.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
SubplotBase: The matplotlib axes object.
|
|
108
|
+
"""
|
|
109
|
+
df["transaction_period"] = df[get_option("column.transaction_date")].dt.to_period(
|
|
110
|
+
period,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if group_col is None:
|
|
114
|
+
default_colors = get_named_color("primary")
|
|
115
|
+
df = df.groupby("transaction_period")[value_col].agg(agg_func)
|
|
116
|
+
default_title = "Total Sales"
|
|
117
|
+
show_legend = False
|
|
118
|
+
else:
|
|
119
|
+
default_colors = get_linear_cmap("green")(
|
|
120
|
+
np.linspace(0, 1, df[group_col].nunique()),
|
|
121
|
+
)
|
|
122
|
+
df = (
|
|
123
|
+
df.groupby([group_col, "transaction_period"])[value_col]
|
|
124
|
+
.agg(agg_func)
|
|
125
|
+
.reset_index()
|
|
126
|
+
.pivot(index="transaction_period", columns=group_col, values=value_col)
|
|
127
|
+
)
|
|
128
|
+
default_title = f"{value_col.title()} by {group_col.title()}"
|
|
129
|
+
show_legend = True
|
|
130
|
+
|
|
131
|
+
linewidth = kwargs.pop("linewidth", 3)
|
|
132
|
+
color = kwargs.pop("color", default_colors)
|
|
133
|
+
ax = df.plot(
|
|
134
|
+
linewidth=linewidth,
|
|
135
|
+
color=color,
|
|
136
|
+
legend=show_legend,
|
|
137
|
+
ax=ax,
|
|
138
|
+
**kwargs,
|
|
139
|
+
)
|
|
140
|
+
ax = gu.standard_graph_styles(
|
|
141
|
+
ax,
|
|
142
|
+
title=gu.not_none(title, default_title),
|
|
143
|
+
x_label=gu.not_none(x_label, ""),
|
|
144
|
+
y_label=gu.not_none(y_label, value_col.title()),
|
|
145
|
+
legend_title=legend_title,
|
|
146
|
+
move_legend_outside=move_legend_outside,
|
|
147
|
+
show_legend=show_legend,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
decimals = gu.get_decimals(ax.get_ylim(), ax.get_yticks())
|
|
151
|
+
ax.yaxis.set_major_formatter(
|
|
152
|
+
lambda x, pos: gu.human_format(x, pos, decimals=decimals),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if source_text is not None:
|
|
156
|
+
gu.add_source_text(ax=ax, source_text=source_text)
|
|
157
|
+
|
|
158
|
+
gu.standard_tick_styles(ax)
|
|
159
|
+
|
|
160
|
+
return ax
|