pytrendy 1.4.0.dev2__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.dev2 → pytrendy-1.4.0.dev4}/PKG-INFO +16 -14
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/README.md +15 -13
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pyproject.toml +1 -1
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/detect_trends.py +2 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/__init__.py +2 -1
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/abrupt_shaving.py +4 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/artifact_cleanup.py +7 -2
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/gradual_expand_contract.py +63 -1
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/process_signals.py +16 -10
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/LICENSE +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/__init__.py +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/__init__.py +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/data/classes_signals.csv +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/data/series_synthetic.csv +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/data_loader.py +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/plot_pytrendy.py +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/results_pytrendy.py +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/__init__.py +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_analyse.py +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_get.py +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/segment_grouping.py +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/trend_classify.py +0 -0
- {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/update_neighbours.py +0 -0
- {pytrendy-1.4.0.dev2 → 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
|
|
|
@@ -115,7 +118,7 @@ results.print_summary()
|
|
|
115
118
|
Detected:
|
|
116
119
|
- 3 Uptrends.
|
|
117
120
|
- 3 Downtrends.
|
|
118
|
-
-
|
|
121
|
+
- 2 Flats.
|
|
119
122
|
- 0 Noise.
|
|
120
123
|
|
|
121
124
|
The best detected trend is Down between dates 2025-05-09 - 2025-06-17
|
|
@@ -126,13 +129,12 @@ Full Results:
|
|
|
126
129
|
time_index
|
|
127
130
|
1 Up 2025-01-02 2025-01-24 22 14.013348 5 gradual
|
|
128
131
|
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
|
|
132
|
+
3 Flat 2025-02-06 2025-02-09 3 -1.168831 8 NaN
|
|
133
|
+
4 Up 2025-02-10 2025-03-17 35 26.190476 3 gradual
|
|
134
|
+
5 Down 2025-03-18 2025-04-01 14 -22.721861 4 gradual
|
|
135
|
+
6 Up 2025-04-02 2025-05-08 36 72.611833 2 gradual
|
|
136
|
+
7 Down 2025-05-09 2025-06-17 39 -73.253968 1 gradual
|
|
137
|
+
8 Flat 2025-06-18 2025-06-30 12 3.910534 7 NaN
|
|
136
138
|
-------------------------------------------------------------------------------
|
|
137
139
|
```
|
|
138
140
|
|
|
@@ -144,7 +146,7 @@ results.filter_segments(direction='Up', sort_by='change_rank')[:3]
|
|
|
144
146
|
| time_index | direction | start | end | trend_class | change | pct_change | days | total_change | SNR | change_rank |
|
|
145
147
|
|---|---|---|---|---|---|---|---|---|---|---|
|
|
146
148
|
| 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-
|
|
149
|
+
| 4 | Up | 2025-02-10 | 2025-03-17 | gradual | 26.19 | 179.93% | 35 | 26.19 | 18.61 | 3 |
|
|
148
150
|
| 1 | Up | 2025-01-02 | 2025-01-24 | gradual | 14.01 | 104.41% | 22 | 14.01 | 22.21 | 5 |
|
|
149
151
|
|
|
150
152
|
`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.
|
|
@@ -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
|
|
|
@@ -52,7 +55,7 @@ results.print_summary()
|
|
|
52
55
|
Detected:
|
|
53
56
|
- 3 Uptrends.
|
|
54
57
|
- 3 Downtrends.
|
|
55
|
-
-
|
|
58
|
+
- 2 Flats.
|
|
56
59
|
- 0 Noise.
|
|
57
60
|
|
|
58
61
|
The best detected trend is Down between dates 2025-05-09 - 2025-06-17
|
|
@@ -63,13 +66,12 @@ Full Results:
|
|
|
63
66
|
time_index
|
|
64
67
|
1 Up 2025-01-02 2025-01-24 22 14.013348 5 gradual
|
|
65
68
|
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
|
|
69
|
+
3 Flat 2025-02-06 2025-02-09 3 -1.168831 8 NaN
|
|
70
|
+
4 Up 2025-02-10 2025-03-17 35 26.190476 3 gradual
|
|
71
|
+
5 Down 2025-03-18 2025-04-01 14 -22.721861 4 gradual
|
|
72
|
+
6 Up 2025-04-02 2025-05-08 36 72.611833 2 gradual
|
|
73
|
+
7 Down 2025-05-09 2025-06-17 39 -73.253968 1 gradual
|
|
74
|
+
8 Flat 2025-06-18 2025-06-30 12 3.910534 7 NaN
|
|
73
75
|
-------------------------------------------------------------------------------
|
|
74
76
|
```
|
|
75
77
|
|
|
@@ -81,7 +83,7 @@ results.filter_segments(direction='Up', sort_by='change_rank')[:3]
|
|
|
81
83
|
| time_index | direction | start | end | trend_class | change | pct_change | days | total_change | SNR | change_rank |
|
|
82
84
|
|---|---|---|---|---|---|---|---|---|---|---|
|
|
83
85
|
| 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-
|
|
86
|
+
| 4 | Up | 2025-02-10 | 2025-03-17 | gradual | 26.19 | 179.93% | 35 | 26.19 | 18.61 | 3 |
|
|
85
87
|
| 1 | Up | 2025-01-02 | 2025-01-24 | gradual | 14.01 | 104.41% | 22 | 14.01 | 22.21 | 5 |
|
|
86
88
|
|
|
87
89
|
`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.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.dev2 → 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
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|