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