pytrendy 1.4.0.dev1__tar.gz → 1.4.0.dev3__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.4.0.dev1 → pytrendy-1.4.0.dev3}/PKG-INFO +9 -10
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/README.md +8 -9
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pyproject.toml +1 -1
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/detect_trends.py +1 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/io/plot_pytrendy.py +10 -1
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/post_processing/segments_refine/abrupt_shaving.py +4 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/process_signals.py +16 -10
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/LICENSE +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/__init__.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/io/__init__.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/io/data/classes_signals.csv +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/io/data/series_synthetic.csv +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/io/data_loader.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/io/results_pytrendy.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/post_processing/__init__.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/post_processing/segments_analyse.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/post_processing/segments_get.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/post_processing/segments_refine/__init__.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/post_processing/segments_refine/artifact_cleanup.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/post_processing/segments_refine/gradual_expand_contract.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/post_processing/segments_refine/segment_grouping.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/post_processing/segments_refine/trend_classify.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/post_processing/segments_refine/update_neighbours.py +0 -0
- {pytrendy-1.4.0.dev1 → pytrendy-1.4.0.dev3}/pytrendy/simpledtw.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytrendy
|
|
3
|
-
Version: 1.4.0.
|
|
3
|
+
Version: 1.4.0.dev3
|
|
4
4
|
Summary: Trend Detection in Python. Applicable for real-world industry use cases in time series.
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -115,7 +115,7 @@ results.print_summary()
|
|
|
115
115
|
Detected:
|
|
116
116
|
- 3 Uptrends.
|
|
117
117
|
- 3 Downtrends.
|
|
118
|
-
-
|
|
118
|
+
- 2 Flats.
|
|
119
119
|
- 0 Noise.
|
|
120
120
|
|
|
121
121
|
The best detected trend is Down between dates 2025-05-09 - 2025-06-17
|
|
@@ -126,13 +126,12 @@ Full Results:
|
|
|
126
126
|
time_index
|
|
127
127
|
1 Up 2025-01-02 2025-01-24 22 14.013348 5 gradual
|
|
128
128
|
2 Down 2025-01-25 2025-02-05 11 -13.564214 6 gradual
|
|
129
|
-
3 Flat 2025-02-06 2025-02-09 3 -1.168831
|
|
130
|
-
4 Up 2025-02-10 2025-03-
|
|
131
|
-
5
|
|
132
|
-
6
|
|
133
|
-
7
|
|
134
|
-
8
|
|
135
|
-
9 Flat 2025-06-18 2025-06-30 12 3.910534 8 NaN
|
|
129
|
+
3 Flat 2025-02-06 2025-02-09 3 -1.168831 8 NaN
|
|
130
|
+
4 Up 2025-02-10 2025-03-17 35 26.190476 3 gradual
|
|
131
|
+
5 Down 2025-03-18 2025-04-01 14 -22.721861 4 gradual
|
|
132
|
+
6 Up 2025-04-02 2025-05-08 36 72.611833 2 gradual
|
|
133
|
+
7 Down 2025-05-09 2025-06-17 39 -73.253968 1 gradual
|
|
134
|
+
8 Flat 2025-06-18 2025-06-30 12 3.910534 7 NaN
|
|
136
135
|
-------------------------------------------------------------------------------
|
|
137
136
|
```
|
|
138
137
|
|
|
@@ -144,7 +143,7 @@ results.filter_segments(direction='Up', sort_by='change_rank')[:3]
|
|
|
144
143
|
| time_index | direction | start | end | trend_class | change | pct_change | days | total_change | SNR | change_rank |
|
|
145
144
|
|---|---|---|---|---|---|---|---|---|---|---|
|
|
146
145
|
| 7 | Up | 2025-04-02 | 2025-05-08 | gradual | 72.61 | 367.50% | 36 | 72.61 | 21.70 | 2 |
|
|
147
|
-
| 4 | Up | 2025-02-10 | 2025-03-
|
|
146
|
+
| 4 | Up | 2025-02-10 | 2025-03-17 | gradual | 26.19 | 179.93% | 35 | 26.19 | 18.61 | 3 |
|
|
148
147
|
| 1 | Up | 2025-01-02 | 2025-01-24 | gradual | 14.01 | 104.41% | 22 | 14.01 | 22.21 | 5 |
|
|
149
148
|
|
|
150
149
|
`filter_segments` ranks segments by magnitude (`change_rank`). See the [API reference](https://russellsb.github.io/pytrendy/main/reference/pytrendy/io/results_pytrendy/#pytrendy.io.results_pytrendy.PyTrendyResults.filter_segments) for all filter and sort options.
|
|
@@ -52,7 +52,7 @@ results.print_summary()
|
|
|
52
52
|
Detected:
|
|
53
53
|
- 3 Uptrends.
|
|
54
54
|
- 3 Downtrends.
|
|
55
|
-
-
|
|
55
|
+
- 2 Flats.
|
|
56
56
|
- 0 Noise.
|
|
57
57
|
|
|
58
58
|
The best detected trend is Down between dates 2025-05-09 - 2025-06-17
|
|
@@ -63,13 +63,12 @@ Full Results:
|
|
|
63
63
|
time_index
|
|
64
64
|
1 Up 2025-01-02 2025-01-24 22 14.013348 5 gradual
|
|
65
65
|
2 Down 2025-01-25 2025-02-05 11 -13.564214 6 gradual
|
|
66
|
-
3 Flat 2025-02-06 2025-02-09 3 -1.168831
|
|
67
|
-
4 Up 2025-02-10 2025-03-
|
|
68
|
-
5
|
|
69
|
-
6
|
|
70
|
-
7
|
|
71
|
-
8
|
|
72
|
-
9 Flat 2025-06-18 2025-06-30 12 3.910534 8 NaN
|
|
66
|
+
3 Flat 2025-02-06 2025-02-09 3 -1.168831 8 NaN
|
|
67
|
+
4 Up 2025-02-10 2025-03-17 35 26.190476 3 gradual
|
|
68
|
+
5 Down 2025-03-18 2025-04-01 14 -22.721861 4 gradual
|
|
69
|
+
6 Up 2025-04-02 2025-05-08 36 72.611833 2 gradual
|
|
70
|
+
7 Down 2025-05-09 2025-06-17 39 -73.253968 1 gradual
|
|
71
|
+
8 Flat 2025-06-18 2025-06-30 12 3.910534 7 NaN
|
|
73
72
|
-------------------------------------------------------------------------------
|
|
74
73
|
```
|
|
75
74
|
|
|
@@ -81,7 +80,7 @@ results.filter_segments(direction='Up', sort_by='change_rank')[:3]
|
|
|
81
80
|
| time_index | direction | start | end | trend_class | change | pct_change | days | total_change | SNR | change_rank |
|
|
82
81
|
|---|---|---|---|---|---|---|---|---|---|---|
|
|
83
82
|
| 7 | Up | 2025-04-02 | 2025-05-08 | gradual | 72.61 | 367.50% | 36 | 72.61 | 21.70 | 2 |
|
|
84
|
-
| 4 | Up | 2025-02-10 | 2025-03-
|
|
83
|
+
| 4 | Up | 2025-02-10 | 2025-03-17 | gradual | 26.19 | 179.93% | 35 | 26.19 | 18.61 | 3 |
|
|
85
84
|
| 1 | Up | 2025-01-02 | 2025-01-24 | gradual | 14.01 | 104.41% | 22 | 14.01 | 22.21 | 5 |
|
|
86
85
|
|
|
87
86
|
`filter_segments` ranks segments by magnitude (`change_rank`). See the [API reference](https://russellsb.github.io/pytrendy/main/reference/pytrendy/io/results_pytrendy/#pytrendy.io.results_pytrendy.PyTrendyResults.filter_segments) for all filter and sort options.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pytrendy"
|
|
3
|
-
version = "1.4.0.
|
|
3
|
+
version = "1.4.0.dev3"
|
|
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" },
|
|
@@ -44,6 +44,7 @@ def detect_trends(df: pd.DataFrame, date_col: str, value_col: str, plot=True, me
|
|
|
44
44
|
- **avoid_noise** (`bool`): Whether to avoid noisy segments in trend detection. Defaults to `True`.
|
|
45
45
|
plot_params (dict, optional):
|
|
46
46
|
Optional dict to customise plot appearance. Only used when `plot` is `True`. Supported keys:
|
|
47
|
+
|
|
47
48
|
- **figsize** (`tuple`): Figure size as (width, height). Defaults to (20, 5).
|
|
48
49
|
- **title** (`str`): Plot title. Defaults to "PyTrendy Detection".
|
|
49
50
|
- **xlabel** (`str`): X-axis label. Defaults to "Date".
|
|
@@ -23,6 +23,7 @@ def plot_pytrendy(df: pd.DataFrame, value_col: str, segments_enhanced: list[dict
|
|
|
23
23
|
If True, suppresses the automatic display of the plot with plt.show(). Defaults to False.
|
|
24
24
|
plot_params (dict, optional):
|
|
25
25
|
Optional dict to customise plot appearance. Supported keys:
|
|
26
|
+
|
|
26
27
|
- **figsize** (`tuple`): Figure size as (width, height). Defaults to (20, 5).
|
|
27
28
|
- **title** (`str`): Plot title. Defaults to "PyTrendy Detection".
|
|
28
29
|
- **xlabel** (`str`): X-axis label. Defaults to "Date".
|
|
@@ -57,6 +58,8 @@ def plot_pytrendy(df: pd.DataFrame, value_col: str, segments_enhanced: list[dict
|
|
|
57
58
|
}
|
|
58
59
|
if plot_params:
|
|
59
60
|
plot_params = dict(plot_params) # avoid mutating caller's dict
|
|
61
|
+
has_custom_legend_loc = 'legend_loc' in plot_params
|
|
62
|
+
has_custom_legend_anchor = 'legend_bbox_to_anchor' in plot_params
|
|
60
63
|
custom_colors = plot_params.pop('colors', None)
|
|
61
64
|
custom_grid = plot_params.pop('grid', None)
|
|
62
65
|
default_params.update(plot_params)
|
|
@@ -64,6 +67,8 @@ def plot_pytrendy(df: pd.DataFrame, value_col: str, segments_enhanced: list[dict
|
|
|
64
67
|
default_params['colors'].update(custom_colors)
|
|
65
68
|
if custom_grid:
|
|
66
69
|
default_params['grid'].update(custom_grid)
|
|
70
|
+
if has_custom_legend_loc and not has_custom_legend_anchor:
|
|
71
|
+
default_params['legend_bbox_to_anchor'] = None
|
|
67
72
|
|
|
68
73
|
# Define colors
|
|
69
74
|
color_map = default_params['colors']
|
|
@@ -173,7 +178,11 @@ def plot_pytrendy(df: pd.DataFrame, value_col: str, segments_enhanced: list[dict
|
|
|
173
178
|
plt.setp(ax.get_xticklabels(), rotation=90, ha='right')
|
|
174
179
|
|
|
175
180
|
# Optional: show grid lines for both
|
|
176
|
-
|
|
181
|
+
grid_params = default_params['grid']
|
|
182
|
+
if grid_params['visible']:
|
|
183
|
+
ax.grid(True, **{k: v for k, v in grid_params.items() if k != 'visible'})
|
|
184
|
+
else:
|
|
185
|
+
ax.grid(False, which='both')
|
|
177
186
|
|
|
178
187
|
ax.set_title(default_params['title'], fontsize=20)
|
|
179
188
|
ax.set_xlabel(default_params['xlabel'])
|
|
@@ -134,6 +134,10 @@ def shave_abrupt_trends(df: pd.DataFrame, value_col: str, segments: list[dict],
|
|
|
134
134
|
if segment['direction'] not in ['Up', 'Down'] or segment['trend_class'] != 'abrupt':
|
|
135
135
|
continue
|
|
136
136
|
|
|
137
|
+
# Skip segments already padded in first pass; .get returns False when key absent (unpadded segments)
|
|
138
|
+
if second_pass and segment.get('padded', False):
|
|
139
|
+
continue
|
|
140
|
+
|
|
137
141
|
abrupt_start = pd.to_datetime(segment['start'])
|
|
138
142
|
abrupt_end = pd.to_datetime(segment['end'])
|
|
139
143
|
|
|
@@ -53,6 +53,7 @@ def process_signals(df: pd.DataFrame, value_col: str, method_params: dict, debug
|
|
|
53
53
|
|
|
54
54
|
THRESHOLD_NOISE = 2.5 # Sensitivity to detecting noise (recommended 0-10)
|
|
55
55
|
THRESHOLD_SMOOTH = 0.001 # Sensitivity to detecting trends as fraction of iqr
|
|
56
|
+
THRESHOLD_FLAT = 0.835 # Sensitivity to detecting flats as a fraction of min std (non-zero)
|
|
56
57
|
|
|
57
58
|
# 1. Noise detection via SNR.
|
|
58
59
|
# 1.1 Compute the SNR
|
|
@@ -174,31 +175,36 @@ def process_signals(df: pd.DataFrame, value_col: str, method_params: dict, debug
|
|
|
174
175
|
|
|
175
176
|
# 3. Flat detection using rolling std of savgol filter.
|
|
176
177
|
# with leading and trailing to cater for periods centred windows doesnt cover
|
|
178
|
+
|
|
179
|
+
# 3.1 Compute smoothed signal rolling std to be used by following logic
|
|
177
180
|
df['smoothed'] = savgol_filter(df['value_cleaned'], window_length=WINDOW_SMOOTH, polyorder=1)
|
|
178
181
|
df['smoothed_std'] = df['smoothed'].rolling(WINDOW_FLAT, center=True).std()
|
|
179
182
|
df['smoothed_std_leading'] = df['smoothed'].iloc[::-1].rolling(window=WINDOW_FLAT).std().iloc[::-1]
|
|
180
183
|
df['smoothed_std_trailing'] = df['smoothed'].rolling(WINDOW_FLAT).std()
|
|
181
184
|
df['smoothed_std'] = df['smoothed_std'].fillna(df['smoothed_std_leading']).fillna(df['smoothed_std_trailing'])
|
|
182
185
|
|
|
186
|
+
# 3.2 Compute derivative early so flat detection can check both smoothness and motion.
|
|
187
|
+
derivative_limit = abs(iqr(df[value_col])) * THRESHOLD_SMOOTH
|
|
188
|
+
df['smoothed_deriv'] = savgol_filter(df[value_col], window_length=WINDOW_SMOOTH, polyorder=1, deriv=1)
|
|
189
|
+
|
|
190
|
+
# 3.3 Compute Flat Flag using both std and deriv limits
|
|
183
191
|
df['flat_flag'] = 0
|
|
184
192
|
rolling_std = df['value_cleaned'].rolling(WINDOW_FLAT, center=True).std()
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
nonzero_std = rolling_std[rolling_std > 0]
|
|
194
|
+
min_nonzero_std = nonzero_std.min() if not nonzero_std.empty else 0.0
|
|
195
|
+
derivative_near_zero = df['smoothed_deriv'].abs() <= derivative_limit
|
|
196
|
+
extremely_smooth = df['smoothed_std'] < (min_nonzero_std * THRESHOLD_FLAT)
|
|
197
|
+
df.loc[(df['smoothed_std'] <= min_nonzero_std) & (df['noise_flag'] == 0) & (derivative_near_zero | extremely_smooth), 'flat_flag'] = 1
|
|
187
198
|
|
|
188
|
-
# 4. Detect up/down trend.
|
|
189
|
-
# Savgol filter (rolling avg improvement). Caters for seasonality with tightness to day.
|
|
190
|
-
# Results in signal that's uptrend > 0, else down. As long as its not on a flat or noise.
|
|
199
|
+
# 4. Detect up/down trend.
|
|
191
200
|
df['trend_flag'] = 0
|
|
192
201
|
df.loc[df['flat_flag'] == 1, 'trend_flag'] = -2
|
|
193
202
|
df.loc[df['noise_flag'] == 1, 'trend_flag'] = -3
|
|
194
203
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if method_params['avoid_noise']: # noise can be optionally avoided, up to the user
|
|
204
|
+
avoid_condition = (df['flat_flag'] == 0)
|
|
205
|
+
if method_params['avoid_noise']:
|
|
198
206
|
avoid_condition &= (df['noise_flag'] == 0)
|
|
199
207
|
|
|
200
|
-
derivative_limit = abs(iqr(df[value_col])) * THRESHOLD_SMOOTH
|
|
201
|
-
df['smoothed_deriv'] = savgol_filter(df[value_col], window_length=WINDOW_SMOOTH, polyorder=1, deriv=1)
|
|
202
208
|
df.loc[(df['smoothed_deriv'] >= derivative_limit) & avoid_condition, 'trend_flag'] = 1
|
|
203
209
|
df.loc[(df['smoothed_deriv'] < -derivative_limit) & avoid_condition, 'trend_flag'] = -1
|
|
204
210
|
|
|
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.4.0.dev1 → pytrendy-1.4.0.dev3}/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
|