Trend Analysis skill for Codex
Tell a real trend from a hype spike. Codex turns 'X is taking off' into a testable claim, pulls several independent signals such as package downloads, pageviews, job postings and survey waves, and normalises them for category growth, seasonality and base effects. A bundled script runs Mann-Kendall and Sen's slope, and the finding lists which signals agree and which alternative explanations were ruled out.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- trend-analysis Installs to ~/.agents/skills/trend-analysis/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install trend-analysis The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project trend-analysis Installs to .agents/skills/trend-analysis/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/trend-analysis.zipunzip -o trend-analysis.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) trend-analysis PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $trend-analysis, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Spots and validates trends in technology, product and market data - npm and PyPI downloads, Google Trends, Wikipedia pageviews, GitHub activity, job postings, survey waves and internal metrics - by normalising for base effects and seasonality, testing with Mann-Kendall and Sen's slope, triangulating independent signals and separating hype from adoption. Use when asked whether something is trending, growing or declining, to back a "rise of X" claim with data, or to check if a metric's recent movement is real.
Use it when
- Someone claims a technology or market is growing or dying
- A metric moved recently and you need to know if the change is real
- A talk, post or strategy doc needs trend claims backed by data
Use something else when
- You need a forecast of future values: use time-series-forecasting
- You are evaluating one specific library for adoption: use technology-evaluation
- You need to monitor named competitors' changes over time: use competitive-intelligence
How to check the result
Raw data is saved with retrieval dates, trend_check.py reproduces the reported statistics, and the trend appears in at least two independent signals or the report says it does not.
The mistake it prevents
Reading raw download growth as adoption when the whole registry grew, or when one CI pipeline started pulling the package thousands of times a day.
A trend is real when it persists beyond seasonality and noise, shows in more than one independent signal, and survives the obvious alternative explanations (a bot, a definition change, a single viral event). Report the size and the uncertainty, not just the direction.
Workflow
- [ ] 1. State the claim and the population it is about
- [ ] 2. Pick 2-4 independent signals (adoption, interest, supply, outcomes)
- [ ] 3. Pull data with a long enough baseline; store raw files
- [ ] 4. Clean: definition changes, bots, gaps, outliers
- [ ] 5. Normalise: per capita / share, seasonality, base effects
- [ ] 6. Test the trend; size it
- [ ] 7. Triangulate and check alternative explanations
- [ ] 8. Write the finding with confidence and caveats
1-2. Claim and signals
“Rust is growing” is not testable. “Rust’s share of new backend projects at companies like ours grew over the last 24 months” is. Then choose signals that measure different things, so they fail differently:
| Signal type | Examples | Measures | Weakness |
|---|---|---|---|
| Usage | npm/PyPI/crates.io downloads, Docker pulls, API calls | Installs, often CI-driven | Bots, mirrors, CI caches inflate; one big user dominates |
| Interest | Google Trends, Wikipedia pageviews, search volume | Attention | Hype and news spikes; relative indexes |
| Builder activity | New repos, commits, contributors, Stack Overflow questions | Developer engagement | Stars are cheap; SO activity declining overall |
| Supply / demand | Job postings, courses, vendors entering | Market commitment | Lags reality; posting duplication |
| Stated | Annual developer surveys (Stack Overflow, JetBrains, State of JS) | Self-reported use | Sample shifts each year; wording changes |
| Outcomes | Revenue, paid seats, production incidents | Real commitment | Often private |
Hype shows in interest signals first; adoption shows in usage, supply and outcome signals. A trend only in interest data is attention, not adoption.
3. Pulling data (needs network)
# npm daily downloads for a date range (max ~18 months per request)
curl -s "https://api.npmjs.org/downloads/range/2025-01-01:2026-06-30/vite" | jq -r '.downloads[] | [.day, .downloads] | @csv'
# PyPI recent totals (for history, use the public PyPI BigQuery dataset)
curl -s https://pypistats.org/api/packages/polars/overall | jq '.data[:5]'
# Wikipedia monthly pageviews, human traffic only
curl -s "https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/en.wikipedia/all-access/user/Rust_(programming_language)/monthly/20220101/20260101" | jq -r '.items[] | [.timestamp, .views] | @csv'
# Hacker News mentions over time (Algolia API)
curl -s "https://hn.algolia.com/api/v1/search_by_date?query=htmx&tags=story&hitsPerPage=100"
Save raw responses with the retrieval date under a data/raw/ folder so the analysis
can be rerun. Some APIs ask for a contact User-Agent; respect rate limits.
Google Trends values are relative (0-100, scaled to the peak within the chosen query, time range and region) and sampled. Compare terms only within the same request, and never stitch separately downloaded ranges without overlap-based rescaling.
4. Cleaning
- Definition changes: survey questions reworded, package renamed or split, analytics tracking changed. Mark the break; do not compare across it naively.
- Bot and CI traffic: a step jump in downloads on one day usually means a CI pipeline or mirror, not new users. Check whether a popular dependent started depending on it.
- Gaps and partial periods: drop the current incomplete week or month.
- Outliers: keep them in raw data, explain them, and test with and without.
5. Normalise
- Share, not counts: total npm downloads grow every year; compare against the category (e.g. vite vs the sum of bundlers) or the registry total.
- Seasonality: developer activity dips in late December and in summer; weekends are low. Compare year over year or use weekly/monthly aggregates and a seasonal test.
- Base effects: 300% growth from 1,000 is not comparable to 20% growth from 10M. Report absolute and relative change together.
- Log scale for multi-order-of-magnitude series; linear scale for share data.
6. Test and size
The bundled script runs Mann-Kendall (non-parametric, insensitive to outliers) with Sen’s slope, and a seasonal variant:
python3 scripts/trend_check.py data/vite-monthly.csv --period 12
python3 scripts/trend_check.py data/weekly-signups.csv --window 4
Input is a CSV with a header and period,value rows in time order. It prints the test
statistic, p-value, slope per step relative to the median level, and the last-window vs
previous-window change, then a verdict. Interpret:
- Significant and practically large: a trend, size it with Sen’s slope.
- Significant but tiny relative slope: real drift, probably not important.
- Not significant: say “no clear trend”; recent movement may be noise or a cycle.
For changepoints (when did it start?), plot the series with a rolling median and look for
the break; corroborate with a dated event (release, pricing change, viral post). For
forecasting the trend forward, hand off to $time-series-forecasting.
7. Triangulate and challenge
Before reporting, check each alternative explanation:
- Did one large company or dependent cause it?
- Is it a news or launch spike that decays within weeks?
- Did measurement change (tracking, survey sample, API definition)?
- Is the whole category growing, so the share is flat?
- Survivorship: are you only looking at tools that are still around?
Gotchas
- GitHub stars track attention and can be bought or bot-inflated; never use them alone.
- Download counts double with every new CI cache miss pattern; prefer unique-ish signals or share metrics.
- Survey “most loved” or “most admired” is not usage.
- Short windows (under ~2 seasonal cycles) cannot separate trend from seasonality.
- A chart starting at the trough exaggerates growth; show a baseline of at least 2 years when available.
- Correlation between two trending series is almost guaranteed; do not infer causation.
Verification
- Raw data files and retrieval dates are saved; the script reruns to the same numbers.
- The trend holds in at least two independent signals, or the finding says it does not.
- Seasonality and base effects are addressed explicitly.
- Each alternative explanation is checked or listed as unresolved.
Output template
# Is <X> growing? (<date>)
**Finding:** <X>'s share of <category> downloads rose from 18% to 27% over 24 months
(Sen's slope +0.4 pts/month, seasonal MK p < 0.01). **Confidence:** moderate.
| Signal | Window | Change | Test | Supports? |
|---|---|---|---|---|
| npm share of bundler downloads | 24 mo | +9 pts | MK p<0.01 | yes |
| Wikipedia pageviews | 24 mo | +4% | not significant | no (interest flat) |
| Job postings mentioning X | 12 mo | +35% | - | yes |
**Alternative explanations checked:** <list>
**Caveats:** <CI inflation, single-source data, definition changes>
**Data:** data/raw/*.json retrieved <date> What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Trend Analysis"
short_description: "Tell a real trend from a hype spike"
default_prompt: "$trend-analysis Is htmx actually growing or was it a 2023 hype spike? Show me the data" scripts/trend_check.py
#!/usr/bin/env python3
"""Test whether a time series has a real monotonic trend.
Reads a CSV with a header and two columns: period label (date) and value, in time
order. Reports the Mann-Kendall test (seasonal variant with --period), Sen's slope,
the slope relative to the median level, and the last-window vs previous-window change.
Usage:
python3 trend_check.py series.csv # non-seasonal
python3 trend_check.py series.csv --period 12 # monthly data with yearly seasonality
python3 trend_check.py series.csv --window 3 --alpha 0.05
Exits 1 on unreadable input or fewer than 8 points.
"""
import argparse
import csv
import math
import statistics
import sys
from collections import Counter
def fail(msg):
print(f"error: {msg}", file=sys.stderr)
sys.exit(1)
def mk_stats(x):
"""Return (S, variance) for Mann-Kendall with tie correction."""
n = len(x)
s = 0
for i in range(n - 1):
for j in range(i + 1, n):
d = x[j] - x[i]
s += (d > 0) - (d < 0)
ties = Counter(x).values()
var = (n * (n - 1) * (2 * n + 5) - sum(t * (t - 1) * (2 * t + 5) for t in ties)) / 18
return s, var
def z_and_p(s, var):
if var <= 0:
return 0.0, 1.0
z = (s - 1) / math.sqrt(var) if s > 0 else (s + 1) / math.sqrt(var) if s < 0 else 0.0
p = 2 * (1 - 0.5 * (1 + math.erf(abs(z) / math.sqrt(2))))
return z, p
def sen_slope(x, period=None):
slopes = []
if period:
for k in range(period):
season = x[k::period]
slopes += [(season[j] - season[i]) / (j - i) for i in range(len(season)) for j in range(i + 1, len(season))]
return statistics.median(slopes) / period if slopes else 0.0 # per step
slopes = [(x[j] - x[i]) / (j - i) for i in range(len(x)) for j in range(i + 1, len(x))]
return statistics.median(slopes)
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("csv")
ap.add_argument("--period", type=int, help="seasonal period in steps (12 monthly, 52 weekly, 7 daily)")
ap.add_argument("--window", type=int, help="steps per comparison window (default: period or 4)")
ap.add_argument("--alpha", type=float, default=0.05)
a = ap.parse_args()
try:
with open(a.csv, newline="", encoding="utf-8-sig") as f:
rows = list(csv.reader(f))[1:]
labels = [r[0] for r in rows if len(r) >= 2 and r[1].strip()]
x = [float(r[1]) for r in rows if len(r) >= 2 and r[1].strip()]
except (OSError, ValueError, IndexError) as e:
fail(f"cannot read series: {e}")
if len(x) < 8:
fail(f"need at least 8 points, got {len(x)}")
if a.period and len(x) < 2 * a.period:
fail(f"seasonal test needs at least two full periods ({2 * a.period} points)")
if a.period:
s = var = 0
for k in range(a.period):
sk, vk = mk_stats(x[k::a.period])
s, var = s + sk, var + vk
test = f"Seasonal Mann-Kendall (period {a.period})"
else:
s, var = mk_stats(x)
test = "Mann-Kendall"
z, p = z_and_p(s, var)
slope = sen_slope(x, a.period)
level = statistics.median(x)
rel = 100 * slope / level if level else float("nan")
w = a.window or a.period or 4
last, prev = x[-w:], x[-2 * w:-w]
change = (sum(last) - sum(prev)) / sum(prev) * 100 if len(prev) == w and sum(prev) else None
print(f"Series: {len(x)} points, {labels[0]} .. {labels[-1]}")
print(f"{test}: S={s}, z={z:.2f}, p={p:.4f}")
print(f"Sen's slope: {slope:.4g} per step ({rel:+.2f}% of median level per step)")
if change is not None:
print(f"Last {w} steps vs previous {w}: {change:+.1f}%")
if p < a.alpha:
verdict = "INCREASING" if s > 0 else "DECREASING"
print(f"Verdict: {verdict} trend (p < {a.alpha}).")
if abs(rel) < 0.5:
print("Note: statistically detectable but small relative to the level; check practical size.")
else:
print(f"Verdict: NO significant monotonic trend at alpha={a.alpha}. Treat movement as noise or a cycle.")
if not a.period:
print("Hint: rerun with --period if the data has weekly or yearly seasonality.")
if __name__ == "__main__":
main()
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.