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.
Files changed (24) hide show
  1. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/PKG-INFO +16 -14
  2. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/README.md +15 -13
  3. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pyproject.toml +1 -1
  4. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/detect_trends.py +2 -0
  5. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/__init__.py +2 -1
  6. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/abrupt_shaving.py +4 -0
  7. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/artifact_cleanup.py +7 -2
  8. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/gradual_expand_contract.py +63 -1
  9. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/process_signals.py +16 -10
  10. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/LICENSE +0 -0
  11. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/__init__.py +0 -0
  12. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/__init__.py +0 -0
  13. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/data/classes_signals.csv +0 -0
  14. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/data/series_synthetic.csv +0 -0
  15. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/data_loader.py +0 -0
  16. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/plot_pytrendy.py +0 -0
  17. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/io/results_pytrendy.py +0 -0
  18. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/__init__.py +0 -0
  19. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_analyse.py +0 -0
  20. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_get.py +0 -0
  21. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/segment_grouping.py +0 -0
  22. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/trend_classify.py +0 -0
  23. {pytrendy-1.4.0.dev2 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/update_neighbours.py +0 -0
  24. {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.dev2
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
- ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Gradual-Cropped.gif)
95
- ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Abrupt-Cropped.gif)
96
- ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Noise-Spikes-Cropped.gif)
97
- ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Noise-Random-Cropped.gif)
94
+ ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Gradual.gif)
95
+
96
+ ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Abrupt.gif)
97
+
98
+ ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Noise-Spikes.gif)
99
+
100
+ ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Noise-Random.gif)
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
- - 3 Flats.
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 9 NaN
130
- 4 Up 2025-02-10 2025-03-14 32 24.632035 3 gradual
131
- 5 Flat 2025-03-15 2025-03-17 2 5.660173 7 NaN
132
- 6 Down 2025-03-18 2025-04-01 14 -22.721861 4 gradual
133
- 7 Up 2025-04-02 2025-05-08 36 72.611833 2 gradual
134
- 8 Down 2025-05-09 2025-06-17 39 -73.253968 1 gradual
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-14 | gradual | 24.63 | 169.22% | 32 | 24.63 | 18.87 | 3 |
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
- ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Gradual-Cropped.gif)
32
- ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Abrupt-Cropped.gif)
33
- ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Noise-Spikes-Cropped.gif)
34
- ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Noise-Random-Cropped.gif)
31
+ ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Gradual.gif)
32
+
33
+ ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Abrupt.gif)
34
+
35
+ ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Noise-Spikes.gif)
36
+
37
+ ![](https://raw.githubusercontent.com/RussellSB/pytrendy/refs/heads/develop/plots/Noise-Random.gif)
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
- - 3 Flats.
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 9 NaN
67
- 4 Up 2025-02-10 2025-03-14 32 24.632035 3 gradual
68
- 5 Flat 2025-03-15 2025-03-17 2 5.660173 7 NaN
69
- 6 Down 2025-03-18 2025-04-01 14 -22.721861 4 gradual
70
- 7 Up 2025-04-02 2025-05-08 36 72.611833 2 gradual
71
- 8 Down 2025-05-09 2025-06-17 39 -73.253968 1 gradual
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-14 | gradual | 24.63 | 169.22% | 32 | 24.63 | 18.87 | 3 |
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.dev2"
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
 
@@ -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
- is_padded = is_abrupt and ('padded' in segment) and (segment['padded'] == True)
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
- if trend_ends_too_close or trend_too_small or trend_too_flat:
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
- min_nonzero_std = rolling_std[rolling_std > 0].min()
186
- df.loc[(df['smoothed_std'] <= min_nonzero_std) & (df['noise_flag'] == 0), 'flat_flag'] = 1
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. Uses first derivates of savgol filter (like diff).
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
- # Important condition to establish non-trend segments to avoid detecting trends over
196
- avoid_condition = (df['flat_flag'] == 0) # flat is always avoided
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