Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
ded7d77
First pass at removing datetime logic
May 19, 2026
6c5ef10
Added hacky fixes to test plotting
ChrisMarsden833 May 19, 2026
5c58263
Fixed bug in segments_get that was causing mismatching results
ChrisMarsden833 May 19, 2026
3011b65
further tweaks
ChrisMarsden833 May 28, 2026
44e5fff
Fixes for testing framework
ChrisMarsden833 Jun 4, 2026
d239e93
All tests now pass, still need to work on code coverage. Still needs …
ChrisMarsden833 Jun 6, 2026
7464329
Working implementation that passes existing tests. Still needs work f…
ChrisMarsden833 Jun 9, 2026
056d536
Additional tests added; aiming to get to 100% coverage.
ChrisMarsden833 Jul 6, 2026
137c033
Resolved (most) merge conflicts with develop
ChrisMarsden833 Jul 6, 2026
2458373
Fixed integer-index orphan check; lint cleanup.
opencode-agent[bot] Jul 6, 2026
d552b10
Merge branch 'develop' into nodates
RussellSB Jul 7, 2026
0b85c2d
Merge branch 'develop' into nodates
RussellSB Jul 7, 2026
90e10a8
Merge branch 'develop' into nodates
RussellSB Jul 9, 2026
6c3919b
Merge branch 'develop' into nodates
RussellSB Jul 9, 2026
c4cb005
fix: remove debug artifacts, unused imports, and fix docstring typos
Jul 9, 2026
e8e9b50
docs: update docstrings for consistency with index-type generalization
Jul 9, 2026
1bbb275
refactor: extract index detection and boundary remapping from detect_…
Jul 9, 2026
6589d80
fix: guard string index operations against negative wraparound
Jul 9, 2026
8e30021
fix: guard abrupt shaving loop against out-of-range bounds
Jul 9, 2026
320a194
test: add weekly date index test and extract _check_value() helper
Jul 9, 2026
e2485d4
Merge branch 'develop' into nodates
RussellSB Jul 9, 2026
48f3ed7
Merge branch 'nodates' into fix/cleanup-imports-and-artifacts
RussellSB Jul 9, 2026
1b8a073
Merge branch 'nodates' into docs/docstring-updates
RussellSB Jul 9, 2026
705b3d1
Merge branch 'nodates' into refactor/detect-trends-helpers
RussellSB Jul 9, 2026
51827e0
Merge branch 'nodates' into fix/plot-string-and-datetime
RussellSB Jul 9, 2026
9fbc9cf
Merge branch 'nodates' into fix/abrupt-shaving-bounds
RussellSB Jul 9, 2026
8ee48cc
Merge branch 'nodates' into test/weekly-and-conftest
RussellSB Jul 9, 2026
1a1b025
Merge pull request #214 from RussellSB/fix/cleanup-imports-and-artifacts
ChrisMarsden833 Jul 25, 2026
99ee9b1
Merge pull request #215 from RussellSB/docs/docstring-updates
ChrisMarsden833 Jul 25, 2026
6aaedca
Merge pull request #217 from RussellSB/fix/plot-string-and-datetime
ChrisMarsden833 Jul 25, 2026
417eb11
Merge pull request #218 from RussellSB/fix/abrupt-shaving-bounds
ChrisMarsden833 Jul 25, 2026
8712157
Conflict resolved, merge staged
opencode-agent[bot] Jul 25, 2026
ec98f2e
Merge pull request #216 from RussellSB/refactor/detect-trends-helpers
ChrisMarsden833 Jul 25, 2026
42a44e7
Merge remote-tracking branch 'origin/nodates' into test/weekly-and-co…
opencode-agent[bot] Jul 25, 2026
8dbf843
Merge with develop
ChrisMarsden833 Jul 26, 2026
d152178
fix: add plot_params to plot_pytrendy signature and update test expec…
opencode-agent[bot] Jul 26, 2026
4e58945
Merge pull request #269 from RussellSB/fix/plot-params-and-test-expec…
ChrisMarsden833 Jul 26, 2026
5bbf75a
Merge pull request #219 from RussellSB/test/weekly-and-conftest
ChrisMarsden833 Jul 26, 2026
7ace469
fix: update test expectations and remove deprecated is_abrupt_padded …
opencode-agent[bot] Jul 26, 2026
fd1320e
Merge pull request #270 from RussellSB/fix/test-failure-and-deprecati…
ChrisMarsden833 Jul 26, 2026
3531274
test: add coverage tests for plot_pytrendy, detect_trends, and result…
opencode-agent[bot] Jul 26, 2026
115c932
Merge pull request #271 from RussellSB/coverage/test-coverage
ChrisMarsden833 Jul 26, 2026
590fcb1
test: add coverage tests for remaining uncovered lines
RussellSB Aug 4, 2026
8afd742
refactor: remove unreachable guard in abrupt_shaving
RussellSB Aug 4, 2026
e3e0c9f
fix: replace datetime assumptions with integer arithmetic in artifact…
RussellSB Aug 5, 2026
2e3d0e4
revert: restore original artifact_cleanup.py formatting
RussellSB Aug 5, 2026
284ef68
test: use explicit segment values in coverage tests
RussellSB Aug 5, 2026
1548a47
test: add direct plot_pytrendy tests for prev fill and next noise fil…
RussellSB Aug 5, 2026
1aeba4b
test: convert plot tests to mpl baseline comparison and rename segmen…
RussellSB Aug 5, 2026
77e35f7
Merge pull request #278 from RussellSB/fix/pr-205-coverage
ChrisMarsden833 Aug 11, 2026
56bc716
docs: restore time-series terminology in docstrings
RussellSB Aug 12, 2026
eac6fff
refactor: modularise index handling in detect_trends
RussellSB Aug 12, 2026
e2483d7
test: deduplicate internal index construction via conftest helper
RussellSB Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 140 additions & 10 deletions pytrendy/detect_trends.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,142 @@

import warnings
import pandas as pd
import numpy as np
from .process_signals import process_signals
from .post_processing.segments_get import get_segments
from .post_processing.segments_refine import refine_segments
from .post_processing.segments_analyse import analyse_segments
from .io.plot_pytrendy import plot_pytrendy
from .io.results_pytrendy import PyTrendyResults

def detect_trends(df: pd.DataFrame, date_col: str, value_col: str, plot=True, method_params: dict=None, plot_params: dict=None, debug: bool=False ) -> PyTrendyResults:

def _detect_index_type(df: pd.DataFrame, date_col: str) -> str:
"""
Detect the index type from the date column.

Args:
df (pd.DataFrame): Input DataFrame
date_col (str): Name of the date column

Returns:
str: Index type ('date', 'datetime64', 'integer', 'float', 'string')
"""
if pd.api.types.is_string_dtype(df[date_col]):
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
message="Could not infer format.*"
)
parsed = pd.to_datetime(df[date_col], errors="coerce")

if parsed.notna().all():
return "date"
else:
return "string"
elif pd.api.types.is_datetime64_any_dtype(df[date_col]):
return "datetime64"
elif pd.api.types.is_integer_dtype(df[date_col]):
return "integer"
elif pd.api.types.is_float_dtype(df[date_col]):
return "float"
else:
raise NotImplementedError(f"date_col has unimplemented dtype {df[date_col].dtype}")


def _prepare_index_framework(df: pd.DataFrame, date_col: str | None, value_col: str) -> tuple:
"""
Prepare the internal index framework used by the pipeline.

Detects the index type, captures the external index values, builds the
internal integer index and its lookup, and stages the working DataFrame
on a dedicated scratch column so the user's columns are never clobbered.

Args:
df (pd.DataFrame): Input time series DataFrame.
date_col (str|None): Name of the column representing the external index.
value_col (str): Name of the signal column.

Returns:
tuple: ``(df, external_index, index_lookup, index_type)`` where ``df`` is
the internal-indexed working copy, ``external_index`` holds the original
index values, ``index_lookup`` maps internal to external index values, and
``index_type`` is the detected index type.
"""
df = df.copy()
index_type = 'integer'

if date_col is not None:
index_type = _detect_index_type(df, date_col)
external_index = df[date_col].copy()

if index_type == 'date':
df[date_col] = pd.to_datetime(df[date_col])
elif index_type == 'string':
print(
f"Attempting to cast {date_col} to date failed, "
"treating as string lookup."
)
else:
external_index = np.arange(len(df))

internal_index = np.arange(len(df))
index_lookup = dict(zip(internal_index, np.asarray(external_index)))

# Use a dedicated scratch column name to avoid clobbering user's columns
_pytrendy_idx = '_pytrendy_idx'
df[_pytrendy_idx] = internal_index.copy()
df.set_index(_pytrendy_idx, inplace=True)
df = df[[value_col]]

return df, external_index, index_lookup, index_type


def _remap_boundaries(segments: list[dict], index_lookup: dict) -> list[dict]:
"""
Remap internal segment boundaries back to external index values.

Args:
segments (list): Segment list with internal index boundaries.
index_lookup (dict): Mapping from internal to external index values.

Returns:
list: Segment list with boundaries expressed in external index values.
"""
for segment in segments:
segment['start'] = index_lookup[segment['start']]
segment['end'] = index_lookup[segment['end']]
return segments


def _prepare_plot_frame(df: pd.DataFrame, date_col: str | None, external_index, index_type: str) -> pd.DataFrame:
"""
Restore the external index onto the working DataFrame for plotting.

Args:
df (pd.DataFrame): Internal-indexed working DataFrame.
date_col (str|None): Name of the external index column.
external_index: External index values captured before staging.
index_type (str): Detected index type.

Returns:
pd.DataFrame: DataFrame with the external index restored for plotting.
"""
if index_type == 'date':
external_index = pd.to_datetime(external_index)

df[date_col] = external_index
df.set_index(date_col, inplace=True)
return df


def detect_trends(df: pd.DataFrame,
value_col: str,
date_col: str|None=None,
plot: bool=True,
method_params: dict|None=None,
plot_params: dict|None=None,
debug: bool=False
) -> PyTrendyResults:
"""
This is the main function that runs trend detection end-to-end.

Expand All @@ -30,10 +158,10 @@ def detect_trends(df: pd.DataFrame, date_col: str, value_col: str, plot=True, me
df (pd.DataFrame):
Input time series data containing at least the specified `date_col` and `value_col`.
The `date_col` must contain datetime-like values (daily frequency recommended).
date_col (str):
Name of the column representing timestamps. This column is converted to datetime and set as the index.
value_col (str):
Name of the column containing the primary signal to analyse for trend detection.
date_col (str|None):
Historically, this represents the name of the column containing dates, but pytrendy now allows for indexes of any type to be used. In general, this column represents a human readable reference to the x-position of the sequence. Normally this would be a date or timestamp, but any unique set of values could be used. Default is 'None', in which case an integer sequence will be generated and used to identify segments.
plot (bool, optional):
If `True`, generates a matplotlib plot showing the detected trend segments over the original signal.
Defaults to `True`.
Expand Down Expand Up @@ -63,11 +191,8 @@ def detect_trends(df: pd.DataFrame, date_col: str, value_col: str, plot=True, me
An object encapsulating the detected segments and associated metadata.
Use this object to access segment statistics, rankings, and export utilities.
"""
df = df.copy()
df[date_col] = pd.to_datetime(df[date_col])
df.set_index(date_col, inplace=True)
df = df[[value_col]]

df, external_index, index_lookup, index_type = _prepare_index_framework(df, date_col, value_col)

if method_params is None:
method_params = {} # Avoid mutable default argument by accepting None and constructing a new dict here

Expand All @@ -91,7 +216,12 @@ def detect_trends(df: pd.DataFrame, date_col: str, value_col: str, plot=True, me
segments = get_segments(df)
segments = refine_segments(df, value_col, segments, method_params)
segments = analyse_segments(df, value_col, segments)
if plot: plot_pytrendy(df, value_col, segments, plot_params=plot_params)

results = PyTrendyResults(segments)
segments = _remap_boundaries(segments, index_lookup)

if plot:
plot_df = _prepare_plot_frame(df, date_col, external_index, index_type)
plot_pytrendy(df=plot_df, value_col=value_col, segments_enhanced=segments, index_type=index_type, plot_params=plot_params)

results = PyTrendyResults(segments=segments, index_type=index_type)
return results
Loading