pytrendy 1.3.0.dev1__tar.gz → 1.4.0.dev1__tar.gz
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.
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/PKG-INFO +2 -2
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/README.md +1 -1
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pyproject.toml +1 -1
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/detect_trends.py +13 -2
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/io/plot_pytrendy.py +53 -20
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/LICENSE +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/__init__.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/io/__init__.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/io/data/classes_signals.csv +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/io/data/series_synthetic.csv +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/io/data_loader.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/io/results_pytrendy.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/post_processing/__init__.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/post_processing/segments_analyse.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/post_processing/segments_get.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/post_processing/segments_refine/__init__.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/post_processing/segments_refine/abrupt_shaving.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/post_processing/segments_refine/artifact_cleanup.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/post_processing/segments_refine/gradual_expand_contract.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/post_processing/segments_refine/segment_grouping.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/post_processing/segments_refine/trend_classify.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/post_processing/segments_refine/update_neighbours.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/process_signals.py +0 -0
- {pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/simpledtw.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytrendy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0.dev1
|
|
4
4
|
Summary: Trend Detection in Python. Applicable for real-world industry use cases in time series.
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -83,7 +83,7 @@ PyTrendy is a robust solution for identifying and analysing trends in time serie
|
|
|
83
83
|
|
|
84
84
|
## Why PyTrendy?
|
|
85
85
|
|
|
86
|
-
Most time series tools give you either a "trend component" (via decomposition) or "changepoints" (the moments of shift). PyTrendy is built for **
|
|
86
|
+
Most time series tools give you either a "trend component" (via decomposition) or "changepoints" (the moments of shift). PyTrendy is built for **automated trend detection**, answering *what trends existed, how strong were they, and when did they start and end?*
|
|
87
87
|
|
|
88
88
|
- **Beyond step changes** - `ruptures` is the gold standard for abrupt shifts, but it doesn't handle gradual slope changes (digital marketing, stocks, energy). PyTrendy detects both in a single run.
|
|
89
89
|
- **The flat/noise problem** - closest peers (`pytrendseries`, `trendet`, `tstrends`) over-fit trends on flat or noisy periods. PyTrendy's signal-processing and post-processing logic ensures trends are only detected when they are precise and valid.
|
|
@@ -20,7 +20,7 @@ PyTrendy is a robust solution for identifying and analysing trends in time serie
|
|
|
20
20
|
|
|
21
21
|
## Why PyTrendy?
|
|
22
22
|
|
|
23
|
-
Most time series tools give you either a "trend component" (via decomposition) or "changepoints" (the moments of shift). PyTrendy is built for **
|
|
23
|
+
Most time series tools give you either a "trend component" (via decomposition) or "changepoints" (the moments of shift). PyTrendy is built for **automated trend detection**, answering *what trends existed, how strong were they, and when did they start and end?*
|
|
24
24
|
|
|
25
25
|
- **Beyond step changes** - `ruptures` is the gold standard for abrupt shifts, but it doesn't handle gradual slope changes (digital marketing, stocks, energy). PyTrendy detects both in a single run.
|
|
26
26
|
- **The flat/noise problem** - closest peers (`pytrendseries`, `trendet`, `tstrends`) over-fit trends on flat or noisy periods. PyTrendy's signal-processing and post-processing logic ensures trends are only detected when they are precise and valid.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pytrendy"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.4.0.dev1"
|
|
4
4
|
description = "Trend Detection in Python. Applicable for real-world industry use cases in time series."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Russell Sammut Bonnici", email = "r.sammutbonnici@gmail.com" },
|
|
@@ -9,7 +9,7 @@ from .post_processing.segments_analyse import analyse_segments
|
|
|
9
9
|
from .io.plot_pytrendy import plot_pytrendy
|
|
10
10
|
from .io.results_pytrendy import PyTrendyResults
|
|
11
11
|
|
|
12
|
-
def detect_trends(df: pd.DataFrame, date_col: str, value_col: str, plot=True, method_params: dict=None, debug: bool=False ) -> PyTrendyResults:
|
|
12
|
+
def detect_trends(df: pd.DataFrame, date_col: str, value_col: str, plot=True, method_params: dict=None, plot_params: dict=None, debug: bool=False ) -> PyTrendyResults:
|
|
13
13
|
"""
|
|
14
14
|
This is the main function that runs trend detection end-to-end.
|
|
15
15
|
|
|
@@ -42,6 +42,17 @@ def detect_trends(df: pd.DataFrame, date_col: str, value_col: str, plot=True, me
|
|
|
42
42
|
|
|
43
43
|
- **abrupt_padding** (`int`): Number of days to pad around abrupt transitions. Defaults to `0`.
|
|
44
44
|
- **avoid_noise** (`bool`): Whether to avoid noisy segments in trend detection. Defaults to `True`.
|
|
45
|
+
plot_params (dict, optional):
|
|
46
|
+
Optional dict to customise plot appearance. Only used when `plot` is `True`. Supported keys:
|
|
47
|
+
- **figsize** (`tuple`): Figure size as (width, height). Defaults to (20, 5).
|
|
48
|
+
- **title** (`str`): Plot title. Defaults to "PyTrendy Detection".
|
|
49
|
+
- **xlabel** (`str`): X-axis label. Defaults to "Date".
|
|
50
|
+
- **ylabel** (`str`): Y-axis label. Defaults to "Value".
|
|
51
|
+
- **colors** (`dict`): Dictionary mapping direction ('Up', 'Down', 'Flat', 'Noise') to matplotlib colors. Defaults to light variants.
|
|
52
|
+
- **alpha** (`float`): Transparency level for shaded regions. Defaults to 0.4.
|
|
53
|
+
- **grid** (`dict`): Grid configuration with keys 'visible' (bool), 'which' (str), 'color' (str), 'alpha' (float).
|
|
54
|
+
- **legend_loc** (`str`): Legend location. Defaults to "upper right".
|
|
55
|
+
- **legend_bbox_to_anchor** (`tuple`): Legend box anchor position. Defaults to (1, 1.15).
|
|
45
56
|
debug (bool, optional):
|
|
46
57
|
If `True` will run in debug mode, outputting various additional plots and print statements. Only recommended for developers of pytrendy.
|
|
47
58
|
Defaults to `False`.
|
|
@@ -79,7 +90,7 @@ def detect_trends(df: pd.DataFrame, date_col: str, value_col: str, plot=True, me
|
|
|
79
90
|
segments = get_segments(df)
|
|
80
91
|
segments = refine_segments(df, value_col, segments, method_params)
|
|
81
92
|
segments = analyse_segments(df, value_col, segments)
|
|
82
|
-
if plot: plot_pytrendy(df, value_col, segments)
|
|
93
|
+
if plot: plot_pytrendy(df, value_col, segments, plot_params=plot_params)
|
|
83
94
|
|
|
84
95
|
results = PyTrendyResults(segments)
|
|
85
96
|
return results
|
|
@@ -5,7 +5,7 @@ import matplotlib.pyplot as plt
|
|
|
5
5
|
import matplotlib.dates as mdates
|
|
6
6
|
import matplotlib.patches as mpatches
|
|
7
7
|
|
|
8
|
-
def plot_pytrendy(df: pd.DataFrame, value_col: str, segments_enhanced: list[dict], suppress_show: bool = False) -> plt.Figure:
|
|
8
|
+
def plot_pytrendy(df: pd.DataFrame, value_col: str, segments_enhanced: list[dict], suppress_show: bool = False, plot_params: dict = None) -> plt.Figure:
|
|
9
9
|
"""
|
|
10
10
|
Visualizes detected trend segments over the original time series signal.
|
|
11
11
|
|
|
@@ -21,21 +21,54 @@ def plot_pytrendy(df: pd.DataFrame, value_col: str, segments_enhanced: list[dict
|
|
|
21
21
|
List of segment dictionaries containing keys like `'start'`, `'end'`, `'direction'`, `'trend_class'`, and `'change_rank'`.
|
|
22
22
|
suppress_show (bool, optional):
|
|
23
23
|
If True, suppresses the automatic display of the plot with plt.show(). Defaults to False.
|
|
24
|
+
plot_params (dict, optional):
|
|
25
|
+
Optional dict to customise plot appearance. Supported keys:
|
|
26
|
+
- **figsize** (`tuple`): Figure size as (width, height). Defaults to (20, 5).
|
|
27
|
+
- **title** (`str`): Plot title. Defaults to "PyTrendy Detection".
|
|
28
|
+
- **xlabel** (`str`): X-axis label. Defaults to "Date".
|
|
29
|
+
- **ylabel** (`str`): Y-axis label. Defaults to "Value".
|
|
30
|
+
- **colors** (`dict`): Dictionary mapping direction ('Up', 'Down', 'Flat', 'Noise') to matplotlib colors. Defaults to light variants.
|
|
31
|
+
- **alpha** (`float`): Transparency level for shaded regions. Defaults to 0.4.
|
|
32
|
+
- **grid** (`dict`): Grid configuration with keys 'visible' (bool), 'which' (str), 'color' (str), 'alpha' (float).
|
|
33
|
+
- **legend_loc** (`str`): Legend location. Defaults to "upper right".
|
|
34
|
+
- **legend_bbox_to_anchor** (`tuple`): Legend box anchor position. Defaults to (1, 1.15).
|
|
24
35
|
|
|
25
36
|
Returns:
|
|
26
37
|
matplotlib.figure.Figure:
|
|
27
38
|
The figure object containing the plot. Can be displayed with `plt.show()` or saved.
|
|
28
39
|
"""
|
|
29
40
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
41
|
+
# Default plotting params
|
|
42
|
+
default_params = {
|
|
43
|
+
'figsize': (20, 5),
|
|
44
|
+
'title': "PyTrendy Detection",
|
|
45
|
+
'xlabel': "Date",
|
|
46
|
+
'ylabel': "Value",
|
|
47
|
+
'colors': {
|
|
48
|
+
'Up': 'lightgreen',
|
|
49
|
+
'Down': 'lightcoral',
|
|
50
|
+
'Flat': 'lightblue',
|
|
51
|
+
'Noise': 'lightgray',
|
|
52
|
+
},
|
|
53
|
+
'alpha': 0.4,
|
|
54
|
+
'grid': {'visible': True, 'which': 'major', 'color': 'gray', 'alpha': 0.3},
|
|
55
|
+
'legend_loc': 'upper right',
|
|
56
|
+
'legend_bbox_to_anchor': (1, 1.15)
|
|
36
57
|
}
|
|
58
|
+
if plot_params:
|
|
59
|
+
plot_params = dict(plot_params) # avoid mutating caller's dict
|
|
60
|
+
custom_colors = plot_params.pop('colors', None)
|
|
61
|
+
custom_grid = plot_params.pop('grid', None)
|
|
62
|
+
default_params.update(plot_params)
|
|
63
|
+
if custom_colors:
|
|
64
|
+
default_params['colors'].update(custom_colors)
|
|
65
|
+
if custom_grid:
|
|
66
|
+
default_params['grid'].update(custom_grid)
|
|
37
67
|
|
|
38
|
-
|
|
68
|
+
# Define colors
|
|
69
|
+
color_map = default_params['colors']
|
|
70
|
+
|
|
71
|
+
fig, ax = plt.subplots(figsize=default_params['figsize'])
|
|
39
72
|
|
|
40
73
|
# Plot the value line
|
|
41
74
|
ax.plot(df.index, df[value_col], color='black', lw=1)
|
|
@@ -84,7 +117,7 @@ def plot_pytrendy(df: pd.DataFrame, value_col: str, segments_enhanced: list[dict
|
|
|
84
117
|
prev_new_end = (prev_end + pd.Timedelta(days=1)).strftime('%Y-%m-%d')
|
|
85
118
|
mask = (df.index >= prev_end) & (df.index <= prev_new_end)
|
|
86
119
|
prev_color = color_map.get(segments_enhanced[i-1]['direction'], 'gray')
|
|
87
|
-
ax.fill_between(df.index[mask], ymin, ymax, color=prev_color, alpha=
|
|
120
|
+
ax.fill_between(df.index[mask], ymin, ymax, color=prev_color, alpha=default_params['alpha'])
|
|
88
121
|
|
|
89
122
|
|
|
90
123
|
# Adjust ends when appropriate
|
|
@@ -108,7 +141,7 @@ def plot_pytrendy(df: pd.DataFrame, value_col: str, segments_enhanced: list[dict
|
|
|
108
141
|
end = end
|
|
109
142
|
|
|
110
143
|
mask = (df.index >= start) & (df.index <= end)
|
|
111
|
-
ax.fill_between(df.index[mask], ymin, ymax, color=color, alpha=
|
|
144
|
+
ax.fill_between(df.index[mask], ymin, ymax, color=color, alpha=default_params['alpha'])
|
|
112
145
|
|
|
113
146
|
# Add ranking if up/down trend
|
|
114
147
|
if 'change_rank' in seg and seg['direction'] in ['Up', 'Down']:
|
|
@@ -140,21 +173,21 @@ def plot_pytrendy(df: pd.DataFrame, value_col: str, segments_enhanced: list[dict
|
|
|
140
173
|
plt.setp(ax.get_xticklabels(), rotation=90, ha='right')
|
|
141
174
|
|
|
142
175
|
# Optional: show grid lines for both
|
|
143
|
-
ax.grid(
|
|
176
|
+
ax.grid(default_params['grid']['visible'], **{k: v for k, v in default_params['grid'].items() if k != 'visible'})
|
|
144
177
|
|
|
145
|
-
ax.set_title(
|
|
146
|
-
ax.set_xlabel(
|
|
147
|
-
ax.set_ylabel(
|
|
178
|
+
ax.set_title(default_params['title'], fontsize=20)
|
|
179
|
+
ax.set_xlabel(default_params['xlabel'])
|
|
180
|
+
ax.set_ylabel(default_params['ylabel'])
|
|
148
181
|
|
|
149
182
|
# Create custom legend handles (colored boxes)
|
|
150
183
|
legend_handles = [
|
|
151
|
-
mpatches.Patch(color='
|
|
152
|
-
mpatches.Patch(color='
|
|
153
|
-
mpatches.Patch(color='
|
|
154
|
-
mpatches.Patch(color='
|
|
184
|
+
mpatches.Patch(color=default_params['colors']['Up'], alpha=default_params['alpha'], label='Up'),
|
|
185
|
+
mpatches.Patch(color=default_params['colors']['Down'], alpha=default_params['alpha'], label='Down'),
|
|
186
|
+
mpatches.Patch(color=default_params['colors']['Flat'], alpha=default_params['alpha'], label='Flat'),
|
|
187
|
+
mpatches.Patch(color=default_params['colors']['Noise'], alpha=default_params['alpha'], label='Noise'),
|
|
155
188
|
]
|
|
156
|
-
ax.legend(handles=legend_handles, loc='
|
|
157
|
-
bbox_to_anchor=
|
|
189
|
+
ax.legend(handles=legend_handles, loc=default_params['legend_loc'],
|
|
190
|
+
bbox_to_anchor=default_params['legend_bbox_to_anchor'], ncol=4, frameon=True)
|
|
158
191
|
|
|
159
192
|
plt.tight_layout()
|
|
160
193
|
if not suppress_show:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytrendy-1.3.0.dev1 → pytrendy-1.4.0.dev1}/pytrendy/post_processing/segments_refine/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|