Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ results.print_summary()
Detected:
- 3 Uptrends.
- 3 Downtrends.
- 3 Flats.
- 2 Flats.
- 0 Noise.

The best detected trend is Down between dates 2025-05-09 - 2025-06-17
Expand All @@ -63,13 +63,12 @@ Full Results:
time_index
1 Up 2025-01-02 2025-01-24 22 14.013348 5 gradual
2 Down 2025-01-25 2025-02-05 11 -13.564214 6 gradual
3 Flat 2025-02-06 2025-02-09 3 -1.168831 9 NaN
4 Up 2025-02-10 2025-03-14 32 24.632035 3 gradual
5 Flat 2025-03-15 2025-03-17 2 5.660173 7 NaN
6 Down 2025-03-18 2025-04-01 14 -22.721861 4 gradual
7 Up 2025-04-02 2025-05-08 36 72.611833 2 gradual
8 Down 2025-05-09 2025-06-17 39 -73.253968 1 gradual
9 Flat 2025-06-18 2025-06-30 12 3.910534 8 NaN
3 Flat 2025-02-06 2025-02-09 3 -1.168831 8 NaN
4 Up 2025-02-10 2025-03-17 35 26.190476 3 gradual
5 Down 2025-03-18 2025-04-01 14 -22.721861 4 gradual
6 Up 2025-04-02 2025-05-08 36 72.611833 2 gradual
7 Down 2025-05-09 2025-06-17 39 -73.253968 1 gradual
8 Flat 2025-06-18 2025-06-30 12 3.910534 7 NaN
-------------------------------------------------------------------------------
```

Expand All @@ -81,7 +80,7 @@ results.filter_segments(direction='Up', sort_by='change_rank')[:3]
| time_index | direction | start | end | trend_class | change | pct_change | days | total_change | SNR | change_rank |
|---|---|---|---|---|---|---|---|---|---|---|
| 7 | Up | 2025-04-02 | 2025-05-08 | gradual | 72.61 | 367.50% | 36 | 72.61 | 21.70 | 2 |
| 4 | Up | 2025-02-10 | 2025-03-14 | gradual | 24.63 | 169.22% | 32 | 24.63 | 18.87 | 3 |
| 4 | Up | 2025-02-10 | 2025-03-17 | gradual | 26.19 | 179.93% | 35 | 26.19 | 18.61 | 3 |
| 1 | Up | 2025-01-02 | 2025-01-24 | gradual | 14.01 | 104.41% | 22 | 14.01 | 22.21 | 5 |

`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.
Expand Down
909 changes: 25 additions & 884 deletions docs/examples/fundamentals/abrupt.ipynb

Large diffs are not rendered by default.

1,076 changes: 84 additions & 992 deletions docs/examples/fundamentals/gradual.ipynb

Large diffs are not rendered by default.

1,432 changes: 126 additions & 1,306 deletions docs/examples/fundamentals/noise.ipynb

Large diffs are not rendered by default.

17 changes: 8 additions & 9 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ results.print_summary()
Detected:
- 3 Uptrends.
- 3 Downtrends.
- 3 Flats.
- 2 Flats.
- 0 Noise.

The best detected trend is Down between dates 2025-05-09 - 2025-06-17
Expand All @@ -82,13 +82,12 @@ Full Results:
time_index
1 Up 2025-01-02 2025-01-24 22 14.013348 5 gradual
2 Down 2025-01-25 2025-02-05 11 -13.564214 6 gradual
3 Flat 2025-02-06 2025-02-09 3 -1.168831 9 NaN
4 Up 2025-02-10 2025-03-14 32 24.632035 3 gradual
5 Flat 2025-03-15 2025-03-17 2 5.660173 7 NaN
6 Down 2025-03-18 2025-04-01 14 -22.721861 4 gradual
7 Up 2025-04-02 2025-05-08 36 72.611833 2 gradual
8 Down 2025-05-09 2025-06-17 39 -73.253968 1 gradual
9 Flat 2025-06-18 2025-06-30 12 3.910534 8 NaN
3 Flat 2025-02-06 2025-02-09 3 -1.168831 8 NaN
4 Up 2025-02-10 2025-03-17 35 26.190476 3 gradual
5 Down 2025-03-18 2025-04-01 14 -22.721861 4 gradual
6 Up 2025-04-02 2025-05-08 36 72.611833 2 gradual
7 Down 2025-05-09 2025-06-17 39 -73.253968 1 gradual
8 Flat 2025-06-18 2025-06-30 12 3.910534 7 NaN
-------------------------------------------------------------------------------
```
</div>
Expand All @@ -101,7 +100,7 @@ results.filter_segments(direction='Up', sort_by='change_rank')[:3]
| time_index | direction | start | end | trend_class | change | pct_change | days | total_change | SNR | change_rank |
|---|---|---|---|---|---|---|---|---|---|---|
| 7 | Up | 2025-04-02 | 2025-05-08 | gradual | 72.61 | 367.50% | 36 | 72.61 | 21.70 | 2 |
| 4 | Up | 2025-02-10 | 2025-03-14 | gradual | 24.63 | 169.22% | 32 | 24.63 | 18.87 | 3 |
| 4 | Up | 2025-02-10 | 2025-03-17 | gradual | 26.19 | 179.93% | 35 | 26.19 | 18.61 | 3 |
| 1 | Up | 2025-01-02 | 2025-01-24 | gradual | 14.01 | 104.41% | 22 | 14.01 | 22.21 | 5 |

`filter_segments` ranks segments by magnitude (`change_rank`). See the [API reference](reference/pytrendy/io/results_pytrendy/#pytrendy.io.results_pytrendy.PyTrendyResults.filter_segments) for all filter and sort options.
Expand Down
Binary file added plots/Abrupt.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added plots/Gradual.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added plots/Noise-Random.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added plots/Noise-Spikes.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plots/pytrendy-gradual.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 149 additions & 0 deletions scripts/generate_gifs/abrupt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Generate Abrupt.gif programmatically.

Recreates the animated GIF showing PyTrendy's abrupt trend detection,
with and without padding, in a two-cycle animation.

Usage:
python scripts/generate_gifs/abrupt.py

Output:
plots/Abrupt.gif
"""

import sys
from pathlib import Path

import pandas as pd
from PIL import Image

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import pytrendy as pt
from scripts.generate_gifs.utils import (
REPO_ROOT, render_frame, save_gif
)

def _crossfade(bottom: Image.Image, top: Image.Image, alpha: float) -> Image.Image:
"""Fade top image out to reveal bottom. alpha=0 shows top, alpha=1 shows bottom."""
return Image.blend(top.convert("RGBA"), bottom.convert("RGBA"), alpha)

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
OUTPUT_PATH = REPO_ROOT / "plots" / "Abrupt.gif"

TITLE1 = "Detect Abrupt Trends"
TITLE2 = "Detect Abrupt Trends with Padding"


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def generate():
df = pt.load_data("series_synthetic")[["date", "abrupt"]]
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date")
value_col = "abrupt"

print("Running detection (no padding) ...")
res1 = pt.detect_trends(df.reset_index(), date_col="date", value_col=value_col, plot=False)
segs1 = res1.segments

print("Running detection (padding=28) ...")
res2 = pt.detect_trends(
df.reset_index(), date_col="date", value_col=value_col,
plot=False, method_params=dict(abrupt_padding=28),
)
segs2 = res2.segments

frames: list[Image.Image] = []
durations: list[int] = []

def R(title, sweep=None, segs=None, ranks=False, ra=1.0, sa=0.4):
frames.append(render_frame(df, value_col, title, sweep, segs, ranks, ra, 12, sa))

def hold(ms):
durations.append(ms)

# ── Cycle 1: no padding ────────────────────────────────────────────
print("Rendering Cycle 1 ...")

# 1. Raw plot (white background)
R(TITLE1); hold(500)

# 2. All segments sweep left to right (blue, green, blue, red, blue)
for i in range(30):
R(TITLE1, sweep=(i + 1) / 30, segs=segs1); hold(40)

# 3. Sweep complete hold (all segments visible)
R(TITLE1, sweep=1.0, segs=segs1); hold(500)

# 4. Ranks fade in (larger, near top)
for i in range(10):
a = (i + 1) / 10
R(TITLE1, sweep=1.0, segs=segs1, ranks=True, ra=a); hold(40)

# 5. Result hold
R(TITLE1, sweep=1.0, segs=segs1, ranks=True); hold(5000)

# 6. Ranks fade out
for i in range(10):
a = max(0.0, 1.0 - (i + 1) / 10)
R(TITLE1, sweep=1.0, segs=segs1, ranks=True, ra=a); hold(40)

# ── Crossfade: Phase 1 end → Phase 2 start ─────────────────────────
# Pre-render the two frames to crossfade between
phase1_end = render_frame(df, value_col, TITLE1, sweep_progress=1.0, segments=segs1)
phase2_start = render_frame(df, value_col, TITLE2)

for i in range(15):
alpha = (i + 1) / 15
frames.append(_crossfade(phase2_start, phase1_end, alpha))
hold(50)

# ── Cycle 2: with padding ──────────────────────────────────────────
print("Rendering Cycle 2 ...")

# 7. Raw plot (new title)
R(TITLE2); hold(500)

# 8. All segments sweep left to right (padded)
for i in range(30):
R(TITLE2, sweep=(i + 1) / 30, segs=segs2); hold(40)

# 9. Sweep complete hold
R(TITLE2, sweep=1.0, segs=segs2); hold(500)

# 10. Ranks fade in
for i in range(10):
a = (i + 1) / 10
R(TITLE2, sweep=1.0, segs=segs2, ranks=True, ra=a); hold(40)

# 11. Result hold
R(TITLE2, sweep=1.0, segs=segs2, ranks=True); hold(5000)

# 12. Ranks fade out
for i in range(10):
a = max(0.0, 1.0 - (i + 1) / 10)
R(TITLE2, sweep=1.0, segs=segs2, ranks=True, ra=a); hold(40)

# ── Crossfade: Phase 2 end → Phase 1 start (seamless loop) ────────
phase2_end = render_frame(df, value_col, TITLE2, sweep_progress=1.0, segments=segs2)
phase1_start = render_frame(df, value_col, TITLE1)

for i in range(15):
alpha = (i + 1) / 15
frames.append(_crossfade(phase1_start, phase2_end, alpha))
hold(50)

# ── Save ───────────────────────────────────────────────────────────
n = len(frames)
total_s = sum(durations) / 1000
print(f"Saving {n} frames ({total_s:.1f}s total) ...")

size_kb = save_gif(frames, durations, OUTPUT_PATH)
print(f"Done -> {OUTPUT_PATH} ({n} frames, {size_kb:.0f} KB)")


if __name__ == "__main__":
generate()
100 changes: 100 additions & 0 deletions scripts/generate_gifs/gradual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Generate Gradual.gif programmatically.

Recreates the animated GIF showing PyTrendy's gradual trend detection
in a single-cycle animation.

Usage:
python scripts/generate_gifs/gradual.py

Output:
plots/Gradual.gif
"""

import sys
from pathlib import Path

import pandas as pd
from PIL import Image

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import pytrendy as pt
from scripts.generate_gifs.utils import (
REPO_ROOT, render_frame, save_gif
)

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
OUTPUT_PATH = REPO_ROOT / "plots" / "Gradual.gif"

TITLE = "Detect Gradual Trends"


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def generate():
df = pt.load_data("series_synthetic")[["date", "gradual"]]
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date")
value_col = "gradual"

print("Running detection ...")
res = pt.detect_trends(df.reset_index(), date_col="date", value_col=value_col, plot=False)
segs = res.segments

frames: list[Image.Image] = []
durations: list[int] = []

def R(title, sweep=None, segs=None, ranks=False, ra=1.0, sa=0.4):
frames.append(render_frame(df, value_col, title, sweep, segs, ranks, ra, 12, sa))

def hold(ms):
durations.append(ms)

# ── Single cycle ───────────────────────────────────────────────────
print("Rendering animation ...")

# 1. Raw plot (white background)
R(TITLE); hold(500)

# 2. All segments sweep left to right (blue, green, blue, red, blue)
for i in range(30):
R(TITLE, sweep=(i + 1) / 30, segs=segs); hold(40)

# 3. Sweep complete hold (all segments visible)
R(TITLE, sweep=1.0, segs=segs); hold(500)

# 4. Ranks fade in (larger, near top)
for i in range(10):
a = (i + 1) / 10
R(TITLE, sweep=1.0, segs=segs, ranks=True, ra=a); hold(40)

# 5. Result hold
R(TITLE, sweep=1.0, segs=segs, ranks=True); hold(5000)

# 6. Ranks fade out
for i in range(10):
a = max(0.0, 1.0 - (i + 1) / 10)
R(TITLE, sweep=1.0, segs=segs, ranks=True, ra=a); hold(40)

# 7. Segments fade out (alpha fade, no sweep)
for i in range(10):
a = max(0.0, 1.0 - (i + 1) / 10)
R(TITLE, sweep=1.0, segs=segs, sa=a * 0.4); hold(40)

# 8. Brief pause on raw plot (matches frame 0 for seamless loop)
R(TITLE); hold(300)

# ── Save ───────────────────────────────────────────────────────────
n = len(frames)
total_s = sum(durations) / 1000
print(f"Saving {n} frames ({total_s:.1f}s total) ...")

size_kb = save_gif(frames, durations, OUTPUT_PATH)
print(f"Done -> {OUTPUT_PATH} ({n} frames, {size_kb:.0f} KB)")


if __name__ == "__main__":
generate()
Loading
Loading