pytrendy 1.4.0.dev3__tar.gz → 1.4.0.dev4__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.dev3 → pytrendy-1.4.0.dev4}/PKG-INFO +8 -5
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/README.md +7 -4
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pyproject.toml +1 -1
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/detect_trends.py +2 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/__init__.py +2 -1
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/artifact_cleanup.py +7 -2
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/gradual_expand_contract.py +63 -1
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/LICENSE +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/__init__.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/__init__.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/data/classes_signals.csv +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/data/series_synthetic.csv +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/data_loader.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/plot_pytrendy.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/results_pytrendy.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/__init__.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_analyse.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_get.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/abrupt_shaving.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/segment_grouping.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/trend_classify.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/update_neighbours.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/process_signals.py +0 -0
- {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/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.dev4
|
|
4
4
|
Summary: Trend Detection in Python. Applicable for real-world industry use cases in time series.
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -91,10 +91,13 @@ Most time series tools give you either a "trend component" (via decomposition) o
|
|
|
91
91
|
|
|
92
92
|
## Features
|
|
93
93
|
|
|
94
|
-

|
|
95
|
+
|
|
96
|
+

|
|
97
|
+
|
|
98
|
+

|
|
99
|
+
|
|
100
|
+

|
|
98
101
|
|
|
99
102
|
## Quickstart
|
|
100
103
|
|
|
@@ -28,10 +28,13 @@ Most time series tools give you either a "trend component" (via decomposition) o
|
|
|
28
28
|
|
|
29
29
|
## Features
|
|
30
30
|
|
|
31
|
-

|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+

|
|
35
38
|
|
|
36
39
|
## Quickstart
|
|
37
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pytrendy"
|
|
3
|
-
version = "1.4.0.
|
|
3
|
+
version = "1.4.0.dev4"
|
|
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" },
|
|
@@ -41,6 +41,7 @@ def detect_trends(df: pd.DataFrame, date_col: str, value_col: str, plot=True, me
|
|
|
41
41
|
Optional parameters to customize detection heuristics. Supported keys:
|
|
42
42
|
|
|
43
43
|
- **abrupt_padding** (`int`): Number of days to pad around abrupt transitions. Defaults to `0`.
|
|
44
|
+
- **gradual_padding** (`int`): Number of days to pad after gradual trend ends. Defaults to `0`.
|
|
44
45
|
- **avoid_noise** (`bool`): Whether to avoid noisy segments in trend detection. Defaults to `True`.
|
|
45
46
|
plot_params (dict, optional):
|
|
46
47
|
Optional dict to customise plot appearance. Only used when `plot` is `True`. Supported keys:
|
|
@@ -83,6 +84,7 @@ def detect_trends(df: pd.DataFrame, date_col: str, value_col: str, plot=True, me
|
|
|
83
84
|
# Configures trend detection heuristics
|
|
84
85
|
method_params = {
|
|
85
86
|
'abrupt_padding': method_params.get('abrupt_padding', 0),
|
|
87
|
+
'gradual_padding': method_params.get('gradual_padding', 0),
|
|
86
88
|
'avoid_noise': method_params.get('avoid_noise', True),
|
|
87
89
|
}
|
|
88
90
|
|
{pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/__init__.py
RENAMED
|
@@ -30,6 +30,7 @@ def refine_segments(df: pd.DataFrame, value_col: str, segments: list[dict], meth
|
|
|
30
30
|
method_params (dict): Optional parameters for abrupt padding and control. Supported keys:
|
|
31
31
|
|
|
32
32
|
- **abrupt_padding** (`int`): Number of days to pad. Defaults to `0`.
|
|
33
|
+
- **gradual_padding** (`int`): Number of days to extend gradual segment ends forward. Defaults to `0`.
|
|
33
34
|
|
|
34
35
|
Returns:
|
|
35
36
|
list: Final refined segment list.
|
|
@@ -40,7 +41,7 @@ def refine_segments(df: pd.DataFrame, value_col: str, segments: list[dict], meth
|
|
|
40
41
|
segments_refined = classify_trends(df, value_col, segments_refined)
|
|
41
42
|
segments_refined = group_segments(segments_refined) # grouping 1st pass: sporadic flats & noises
|
|
42
43
|
|
|
43
|
-
segments_refined = expand_contract_segments(df, value_col, segments_refined) # for gradual
|
|
44
|
+
segments_refined = expand_contract_segments(df, value_col, segments_refined, method_params) # for gradual + gradual padding
|
|
44
45
|
segments_refined = shave_abrupt_trends(df, value_col, segments_refined, method_params) # for abrupt
|
|
45
46
|
|
|
46
47
|
segments_refined = clean_artifacts(df, value_col, segments_refined, method_params) # cleans overlaps etc from expand/contract
|
|
@@ -259,7 +259,10 @@ def clean_artifacts(df: pd.DataFrame, value_col: str, segments_refined: list[dic
|
|
|
259
259
|
is_flat = segment['direction'] == 'Flat'
|
|
260
260
|
is_gradual = ('trend_class' in segment and segment['trend_class'] == 'gradual')
|
|
261
261
|
is_abrupt = ('trend_class' in segment and segment['trend_class'] == 'abrupt')
|
|
262
|
-
|
|
262
|
+
# Covers both abrupt_padding and gradual_padding: padded segments must
|
|
263
|
+
# not be reclassified as Flat by the trend_too_flat / trend_too_small
|
|
264
|
+
# checks below, since the extension was intentional.
|
|
265
|
+
is_padded = ('padded' in segment) and (segment['padded'] == True)
|
|
263
266
|
is_small = len(df_segment) <= 5
|
|
264
267
|
|
|
265
268
|
# Edge case 1: Check SNR for trend but noise
|
|
@@ -316,7 +319,9 @@ def clean_artifacts(df: pd.DataFrame, value_col: str, segments_refined: list[dic
|
|
|
316
319
|
segment['direction'] = 'Noise'
|
|
317
320
|
if 'trend_class' in segment: del segment['trend_class']
|
|
318
321
|
|
|
319
|
-
|
|
322
|
+
# Padded segments are protected from flat reclassification — the extension
|
|
323
|
+
# into flat regions is intentional and the original trend still holds.
|
|
324
|
+
if trend_ends_too_close or trend_too_small or (trend_too_flat and not is_padded):
|
|
320
325
|
segment['direction'] = 'Flat'
|
|
321
326
|
if 'trend_class' in segment: del segment['trend_class']
|
|
322
327
|
|
|
@@ -8,17 +8,21 @@ from copy import deepcopy
|
|
|
8
8
|
from .update_neighbours import update_prev_segment, update_next_segment
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def expand_contract_segments(df: pd.DataFrame, value_col: str, segments: list[dict]) -> list[dict]:
|
|
11
|
+
def expand_contract_segments(df: pd.DataFrame, value_col: str, segments: list[dict], method_params: dict) -> list[dict]:
|
|
12
12
|
"""
|
|
13
13
|
Refines segment boundaries by expanding or contracting based on local extrema.
|
|
14
14
|
|
|
15
15
|
Examines ±7 days around each segment's start and end to find stronger turning points.
|
|
16
16
|
Skips segments classified as 'abrupt' to preserve their precision.
|
|
17
|
+
Optionally pads gradual segments into adjacent flat regions when ``gradual_padding > 0``.
|
|
17
18
|
|
|
18
19
|
Args:
|
|
19
20
|
df (pd.DataFrame): Time series DataFrame.
|
|
20
21
|
value_col (str): Name of the signal column.
|
|
21
22
|
segments (list): List of segment dictionaries.
|
|
23
|
+
method_params (dict): Detection parameters. Supported keys:
|
|
24
|
+
|
|
25
|
+
- **gradual_padding** (`int`): Days to extend gradual segment ends forward. Defaults to `0`.
|
|
22
26
|
|
|
23
27
|
Returns:
|
|
24
28
|
list: Refined segment list with updated boundaries.
|
|
@@ -114,4 +118,62 @@ def expand_contract_segments(df: pd.DataFrame, value_col: str, segments: list[di
|
|
|
114
118
|
segments_refined[i]['end'] = new_end.strftime('%Y-%m-%d')
|
|
115
119
|
update_next_segment(i, new_end, segments, segments_refined)
|
|
116
120
|
|
|
121
|
+
# Pad gradual segments into adjacent flat regions when gradual_padding is set.
|
|
122
|
+
# Mirrors the abrupt padding pattern: called from within the same module rather
|
|
123
|
+
# than exposed as a separate top-level step in segments_refine/__init__.py.
|
|
124
|
+
segments_refined = _pad_gradual_trends(df, segments_refined, method_params)
|
|
125
|
+
|
|
117
126
|
return segments_refined
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _pad_gradual_trends(df: pd.DataFrame, segments: list[dict], method_params: dict) -> list[dict]:
|
|
130
|
+
"""
|
|
131
|
+
Extends gradual segment end dates by a specified number of days.
|
|
132
|
+
|
|
133
|
+
Extends the end date forward, truncating before any non-Flat segment that
|
|
134
|
+
would be overlapped, and clamping to the last index date. Sets a ``padded``
|
|
135
|
+
flag on modified segments.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
df (pd.DataFrame): Time series DataFrame.
|
|
139
|
+
segments (list): List of segment dictionaries.
|
|
140
|
+
method_params (dict): Supported keys:
|
|
141
|
+
|
|
142
|
+
- **gradual_padding** (`int`): Number of days to pad. Defaults to ``0``.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
list: Segment list with padded gradual boundaries.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
gradual_padding = method_params.get('gradual_padding', 0)
|
|
149
|
+
if gradual_padding <= 0:
|
|
150
|
+
return segments
|
|
151
|
+
|
|
152
|
+
segments_padded = deepcopy(segments)
|
|
153
|
+
|
|
154
|
+
meta_df = pd.DataFrame(segments)
|
|
155
|
+
meta_df['start'] = pd.to_datetime(meta_df['start'])
|
|
156
|
+
meta_df['end'] = pd.to_datetime(meta_df['end'])
|
|
157
|
+
|
|
158
|
+
for i, segment in enumerate(segments):
|
|
159
|
+
|
|
160
|
+
if segment['direction'] not in ['Up', 'Down'] or segment.get('trend_class') != 'gradual':
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
gradual_end = pd.to_datetime(segment['end'])
|
|
164
|
+
|
|
165
|
+
new_end = gradual_end + pd.Timedelta(days=gradual_padding)
|
|
166
|
+
overlaps = meta_df.loc[(meta_df['start'] > gradual_end) & (meta_df['start'] <= new_end)]
|
|
167
|
+
overlaps_nonflats = overlaps[overlaps['direction'] != 'Flat']
|
|
168
|
+
|
|
169
|
+
if not overlaps_nonflats.empty:
|
|
170
|
+
first_notflat_overlap = overlaps_nonflats.iloc[0]
|
|
171
|
+
new_end = pd.to_datetime(first_notflat_overlap['start']) - pd.Timedelta(days=1)
|
|
172
|
+
|
|
173
|
+
new_end = min(new_end, df.index[-1])
|
|
174
|
+
segments_padded[i]['end'] = new_end.strftime('%Y-%m-%d')
|
|
175
|
+
update_next_segment(i, new_end, segments, segments_padded)
|
|
176
|
+
|
|
177
|
+
segments_padded[i]['padded'] = new_end != gradual_end
|
|
178
|
+
|
|
179
|
+
return segments_padded
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|