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.
Files changed (24) hide show
  1. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/PKG-INFO +8 -5
  2. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/README.md +7 -4
  3. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pyproject.toml +1 -1
  4. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/detect_trends.py +2 -0
  5. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/__init__.py +2 -1
  6. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/artifact_cleanup.py +7 -2
  7. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/gradual_expand_contract.py +63 -1
  8. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/LICENSE +0 -0
  9. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/__init__.py +0 -0
  10. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/__init__.py +0 -0
  11. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/data/classes_signals.csv +0 -0
  12. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/data/series_synthetic.csv +0 -0
  13. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/data_loader.py +0 -0
  14. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/plot_pytrendy.py +0 -0
  15. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/io/results_pytrendy.py +0 -0
  16. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/__init__.py +0 -0
  17. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_analyse.py +0 -0
  18. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_get.py +0 -0
  19. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/abrupt_shaving.py +0 -0
  20. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/segment_grouping.py +0 -0
  21. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/trend_classify.py +0 -0
  22. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/post_processing/segments_refine/update_neighbours.py +0 -0
  23. {pytrendy-1.4.0.dev3 → pytrendy-1.4.0.dev4}/pytrendy/process_signals.py +0 -0
  24. {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.dev3
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
 
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pytrendy"
3
- version = "1.4.0.dev3"
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
@@ -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
File without changes