Time Series Trend Detector (cpp, written by Claude Code)
envgap__claude-code__cpp-t1-9
Written by a coding agent; not on GitHubWritten 2026-02-27
01 / FAILURE SIGNATURE
As the study recorded it
No identifying execution failure has been captured.
Not a benchmark task.
- The project already builds and runs before the fix, so there is nothing to repair.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
CMakeLists.txt- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
claude-code/cpp-t1 #9 · read the task the agent was given
Claude Code wrote this cpp project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: Time Series Trend Detector Write a program that analyzes time series data to detect trends, seasonal patterns, and anomalies using statistical methods, and produces a visual summary report. FUNCTIONAL REQUIREMENTS: - Accept a CSV file path as a command-line argument with columns for timestamp and one or more numeric value columns - Parse timestamps in multiple formats (ISO 8601, Unix epoch, and common date formats like MM/DD/YYYY, YYYY-MM-DD HH:MM:SS) - Compute a moving average with a configurable window size via --window flag (default: 7 data points) - Detect overall trend direction (increasing, decreasing, stable) using linear regression and report the slope and R-squared value - Detect seasonality by computing autocorrelation at various lags and reporting the dominant period if one exists - Identify anomalies: data points that deviate more than a configurable number of standard deviations from the moving average (--threshold flag, default: 2.0) - Support multiple value columns: analyze each independently and report results for all - Generate a summary report with: trend direction and strength, seasonal period (if any), count and list of anomalies with their timestamps and values, basic statistics (min, max, mean, variance) - Save the report as a JSON file with --output flag (default: trend_report.json) - Export the processed data (original values, moving average, anomaly flags) as a CSV file via --export flag - If no input file is given, generate a sample time series dataset with 365 daily data points containing a linear trend, weekly seasonality, and injected anomalies, then analyze it - Handle missing timestamps and gaps in the series by interpolating or flagging them Create a complete C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include: - Source code - CMakeLists.txt with all dependencies (direct and transitive) pinned to exact versions - README.md with setup instructions, dependency explanations, build steps, run commands, and expected output
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(TrendDetector VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# Eigen 3.4.0
FetchContent_Declare(
eigen
GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
GIT_TAG 3.4.0
)
FetchContent_MakeAvailable(eigen)
# nlohmann/json 3.11.3
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
)
FetchContent_MakeAvailable(nlohmann_json)
# csv-parser 2.3.0
FetchContent_Declare(
csv_parser
GIT_REPOSITORY https://github.com/vincentlaucsb/csv-parser.git
GIT_TAG 2.3.0
)
FetchContent_MakeAvailable(csv_parser)
add_executable(detector detector.cpp)
target_link_libraries(detector
PRIVATE
Eigen3::Eigen
nlohmann_json::nlohmann_json
csv
)
detector.cpp
/**
* Time Series Trend Detector - C++ Trial 1
* Uses Eigen + nlohmann/json + csv-parser for time series analysis.
*/
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <cmath>
#include <algorithm>
#include <numeric>
#include <chrono>
#include <iomanip>
#include <random>
#include <map>
#include <ctime>
#include <Eigen/Dense>
#include <nlohmann/json.hpp>
#include <csv.hpp>
using json = nlohmann::json;
using namespace Eigen;
// --- Data Structures ---
struct TimeSeriesPoint {
std::string timestamp;
double value;
long dayIndex; // days since epoch for calculations
};
struct SummaryStats {
int count;
double mean, stddev, minVal, maxVal, median, q25, q75, skewness, kurtosis;
std::string startDate, endDate;
long durationDays;
};
struct TrendResult {
double linearSlope, linearIntercept, linearRSquared;
double poly2RSquared, poly3RSquared;
int bestPolynomialDegree;
double bestRSquared;
std::string direction;
};
struct SeasonalResult {
int period;
double seasonalStrength, seasonalAmplitude, residualStd, trendComponentMean;
};
struct Anomaly {
int index;
std::string timestamp;
double value;
std::string method;
double score;
};
// --- Utility ---
long parseDateToDays(const std::string& dateStr) {
std::tm tm = {};
std::istringstream ss(dateStr.substr(0, 10));
ss >> std::get_time(&tm, "%Y-%m-%d");
if (ss.fail()) {
// Try alternative format
ss.clear();
ss.str(dateStr.substr(0, 10));
ss >> std::get_time(&tm, "%m/%d/%Y");
}
auto tp = std::chrono::system_clock::from_time_t(std::mktime(&tm));
auto days = std::chrono::duration_cast<std::chrono::hours>(tp.time_since_epoch()).count() / 24;
return days;
}
double percentile(std::vector<double> data, double p) {
std::sort(data.begin(), data.end());
double idx = p / 100.0 * (data.size() - 1);
int lo = static_cast<int>(std::floor(idx));
int hi = static_cast<int>(std::ceil(idx));
if (lo == hi) return data[lo];
return data[lo] + (idx - lo) * (data[hi] - data[lo]);
}
// --- Sample Data Generation ---
void generateSampleData(const std::string& outputPath) {
std::mt19937 rng(42);
std::normal_distribution<double> noise(0, 3);
std::uniform_real_distribution<double> uniform(0, 1);
int nPoints = 730;
std::vector<int> dropIndices = {50, 51, 200, 201, 202, 450};
std::vector<int> anomalyIndices = {100, 250, 400, 550, 680};
std::ofstream file(outputPath);
file << "timestamp,value\n";
int written = 0;
for (int i = 0; i < nPoints; i++) {
if (std::find(dropIndices.begin(), dropIndices.end(), i) != dropIndices.end())
continue;
double trend = 10.0 + (40.0 * i / (nPoints - 1));
double seasonal = 15.0 * std::sin(2 * M_PI * i / 365.25);
double weekly = 5.0 * std::sin(2 * M_PI * i / 7.0);
double n = noise(rng);
double value = trend + seasonal + weekly + n;
if (std::find(anomalyIndices.begin(), anomalyIndices.end(), i) != anomalyIndices.end()) {
value += (uniform(rng) > 0.5 ? 1 : -1) * (30 + uniform(rng) * 20);
}
// Calculate date
int year = 2022, month = 1, day = 1;
std::tm start_tm = {0, 0, 0, day, month - 1, year - 1900};
auto start_time = std::mktime(&start_tm);
auto date_time = start_time + i * 86400;
std::tm* date_tm = std::localtime(&date_time);
file << std::put_time(date_tm, "%Y-%m-%d") << ","
<< std::fixed << std::setprecision(4) << value << "\n";
written++;
}
file.close();
std::cout << "Generated sample data with " << written << " points -> " << outputPath << std::endl;
}
// --- Data Loading ---
std::vector<TimeSeriesPoint> loadData(const std::string& filepath) {
std::vector<TimeSeriesPoint> data;
csv::CSVReader reader(filepath);
std::vector<std::string> colNames = reader.get_col_names();
std::string tsCol, valCol;
for (const auto& col : colNames) {
std::string lower = col;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
if (lower == "timestamp" || lower == "date" || lower == "datetime") tsCol = col;
else if (lower == "value" || lower == "val" || lower == "y") valCol = col;
}
if (tsCol.empty()) tsCol = colNames[0];
if (valCol.empty()) valCol = colNames[1];
for (auto& row : reader) {
try {
std::string ts = row[tsCol].get<>();
double val = row[valCol].get<double>();
long dayIdx = parseDateToDays(ts);
data.push_back({ts, val, dayIdx});
} catch (...) {
continue;
}
}
std::sort(data.begin(), data.end(), [](const auto& a, const auto& b) {
return a.dayIndex < b.dayIndex;
});
return data;
}
// --- Handle Missing Timestamps ---
std::vector<TimeSeriesPoint> handleMissingTimestamps(std::vector<TimeSeriesPoint>& data) {
if (data.size() < 2) return data;
std::vector<long> diffs;
for (size_t i = 1; i < data.size(); i++) {
diffs.push_back(data[i].dayIndex - data[i - 1].dayIndex);
}
std::sort(diffs.begin(), diffs.end());
long medianDiff = diffs[diffs.size() / 2];
if (medianDiff < 1) medianDiff = 1;
std::map<long, double> valueMap;
for (const auto& p : data) {
valueMap[p.dayIndex] = p.value;
}
std::vector<TimeSeriesPoint> filled;
long current = data.front().dayIndex;
long end = data.back().dayIndex;
int filledCount = 0;
while (current <= end) {
if (valueMap.count(current)) {
// Find the original timestamp string
std::string ts;
for (const auto& p : data) {
if (p.dayIndex == current) { ts = p.timestamp; break; }
}
filled.push_back({ts, valueMap[current], current});
} else {
// Interpolate
const TimeSeriesPoint* prev = nullptr;
const TimeSeriesPoint* next = nullptr;
for (const auto& p : data) {
if (p.dayIndex <= current) prev = &p;
if (p.dayIndex > current && !next) next = &p;
}
if (prev && next) {
long totalDays = next->dayIndex - prev->dayIndex;
long elapsed = current - prev->dayIndex;
double ratio = totalDays > 0 ? static_cast<double>(elapsed) / totalDays : 0;
double interpolated = prev->value + ratio * (next->value - prev->value);
filled.push_back({"interpolated", interpolated, current});
filledCount++;
}
}
current += medianDiff;
}
if (filledCount > 0) {
std::cout << "Filled " << filledCount << " missing timestamps via interpolation." << std::endl;
}
return filled;
}
// --- Summary Statistics ---
SummaryStats computeSummaryStats(const std::vector<TimeSeriesPoint>& data) {
std::vector<double> values;
for (const auto& p : data) values.push_back(p.value);
int n = values.size();
double mean = std::accumulate(values.begin(), values.end(), 0.0) / n;
double sq_sum = 0;
for (double v : values) sq_sum += (v - mean) * (v - mean);
double stddev = std::sqrt(sq_sum / (n - 1));
std::vector<double> sorted = values;
std::sort(sorted.begin(), sorted.end());
double skew = 0, kurt = 0;
if (stddev > 0) {
for (double v : values) {
double z = (v - mean) / stddev;
skew += z * z * z;
kurt += z * z * z * z;
}
skew /= n;
kurt = kurt / n - 3;
}
SummaryStats stats;
stats.count = n;
stats.mean = mean;
stats.stddev = stddev;
stats.minVal = sorted.front();
stats.maxVal = sorted.back();
stats.median = percentile(values, 50);
stats.q25 = percentile(values, 25);
stats.q75 = percentile(values, 75);
stats.skewness = skew;
stats.kurtosis = kurt;
stats.startDate = data.front().timestamp;
stats.endDate = data.back().timestamp;
stats.durationDays = data.back().dayIndex - data.front().dayIndex;
return stats;
}
// --- Trend Detection (using Eigen) ---
TrendResult detectTrend(const std::vector<TimeSeriesPoint>& data) {
int n = data.size();
VectorXd y(n);
for (int i = 0; i < n; i++) y(i) = data[i].value;
double yMean = y.mean();
double ssTot = (y.array() - yMean).square().sum();
// Linear fit: y = a + b*x
MatrixXd X1(n, 2);
for (int i = 0; i < n; i++) {
X1(i, 0) = 1.0;
X1(i, 1) = static_cast<double>(i);
}
VectorXd coeffs1 = (X1.transpose() * X1).ldlt().solve(X1.transpose() * y);
VectorXd residuals1 = y - X1 * coeffs1;
double ssRes1 = residuals1.squaredNorm();
double r2Lin = ssTot > 0 ? 1.0 - ssRes1 / ssTot : 0;
// Polynomial degree 2
MatrixXd X2(n, 3);
for (int i = 0; i < n; i++) {
double xi = static_cast<double>(i);
X2(i, 0) = 1.0;
X2(i, 1) = xi;
X2(i, 2) = xi * xi;
}
VectorXd coeffs2 = (X2.transpose() * X2).ldlt().solve(X2.transpose() * y);
double ssRes2 = (y - X2 * coeffs2).squaredNorm();
double r2Poly2 = ssTot > 0 ? 1.0 - ssRes2 / ssTot : 0;
// Polynomial degree 3
MatrixXd X3(n, 4);
for (int i = 0; i < n; i++) {
double xi = static_cast<double>(i);
X3(i, 0) = 1.0;
X3(i, 1) = xi;
X3(i, 2) = xi * xi;
X3(i, 3) = xi * xi * xi;
}
VectorXd coeffs3 = (X3.transpose() * X3).ldlt().solve(X3.transpose() * y);
double ssRes3 = (y - X3 * coeffs3).squaredNorm();
double r2Poly3 = ssTot > 0 ? 1.0 - ssRes3 / ssTot : 0;
TrendResult result;
result.linearSlope = coeffs1(1);
result.linearIntercept = coeffs1(0);
result.linearRSquared = r2Lin;
result.poly2RSquared = r2Poly2;
result.poly3RSquared = r2Poly3;
result.bestPolynomialDegree = 1;
result.bestRSquared = r2Lin;
if (r2Poly2 - r2Lin > 0.02) { result.bestPolynomialDegree = 2; result.bestRSquared = r2Poly2; }
if (r2Poly3 - result.bestRSquared > 0.02) { result.bestPolynomialDegree = 3; result.bestRSquared = r2Poly3; }
if (std::abs(result.linearSlope) < 1e-6) result.direction = "flat";
else if (result.linearSlope > 0) result.direction = "upward";
else result.direction = "downward";
return result;
}
// --- Seasonal Decomposition ---
SeasonalResult decomposeSeasons(const std::vector<TimeSeriesPoint>& data, int period) {
int n = data.size();
std::vector<double> values(n);
for (int i = 0; i < n; i++) values[i] = data[i].value;
// Trend via centered moving average
std::vector<double> trendComp(n, std::nan(""));
int halfP = period / 2;
for (int i = halfP; i < n - halfP; i++) {
double sum = 0;
int count = 0;
for (int j = i - halfP; j <= i + halfP; j++) {
sum += values[j];
count++;
}
trendComp[i] = sum / count;
}
// Detrended
std::vector<double> detrended(n);
for (int i = 0; i < n; i++) {
detrended[i] = std::isnan(trendComp[i]) ? 0 : values[i] - trendComp[i];
}
// Seasonal pattern
std::vector<double> seasonalPattern(period, 0);
std::vector<int> seasonalCount(period, 0);
for (int i = 0; i < n; i++) {
if (!std::isnan(trendComp[i])) {
seasonalPattern[i % period] += detrended[i];
seasonalCount[i % period]++;
}
}
for (int i = 0; i < period; i++) {
if (seasonalCount[i] > 0) seasonalPattern[i] /= seasonalCount[i];
}
// Build components
std::vector<double> seasonal(n), residual(n);
for (int i = 0; i < n; i++) {
seasonal[i] = seasonalPattern[i % period];
residual[i] = std::isnan(trendComp[i]) ? 0 : values[i] - trendComp[i] - seasonal[i];
}
// Seasonal strength
std::vector<double> validResid, validSR;
for (int i = 0; i < n; i++) {
if (!std::isnan(trendComp[i])) {
validResid.push_back(residual[i]);
validSR.push_back(seasonal[i] + residual[i]);
}
}
auto variance = [](const std::vector<double>& v) {
double m = std::accumulate(v.begin(), v.end(), 0.0) / v.size();
double s = 0;
for (double x : v) s += (x - m) * (x - m);
return s / v.size();
};
double varResid = variance(validResid);
double varSR = variance(validSR);
double strength = varSR > 0 ? std::max(0.0, std::min(1.0, 1.0 - varResid / varSR)) : 0;
double ampMin = *std::min_element(seasonalPattern.begin(), seasonalPattern.end());
double ampMax = *std::max_element(seasonalPattern.begin(), seasonalPattern.end());
double trendMean = 0;
int trendCount = 0;
for (double t : trendComp) {
if (!std::isnan(t)) { trendMean += t; trendCount++; }
}
trendMean = trendCount > 0 ? trendMean / trendCount : 0;
double residMean = std::accumulate(validResid.begin(), validResid.end(), 0.0) / validResid.size();
double residVar = 0;
for (double r : validResid) residVar += (r - residMean) * (r - residMean);
double residStd = std::sqrt(residVar / (validResid.size() - 1));
SeasonalResult result;
result.period = period;
result.seasonalStrength = strength;
result.seasonalAmplitude = ampMax - ampMin;
result.residualStd = residStd;
result.trendComponentMean = trendMean;
return result;
}
// --- Anomaly Detection ---
std::vector<Anomaly> detectAnomaliesZScore(const std::vector<TimeSeriesPoint>& data, double threshold = 3.0) {
int n = data.size();
std::vector<double> values(n);
for (int i = 0; i < n; i++) values[i] = data[i].value;
double mean = std::accumulate(values.begin(), values.end(), 0.0) / n;
double sqsum = 0;
for (double v : values) sqsum += (v - mean) * (v - mean);
double stddev = std::sqrt(sqsum / (n - 1));
std::vector<Anomaly> anomalies;
if (stddev == 0) return anomalies;
for (int i = 0; i < n; i++) {
double z = std::abs((values[i] - mean) / stddev);
if (z > threshold) {
anomalies.push_back({i, data[i].timestamp, values[i], "z-score", z});
}
}
return anomalies;
}
std::vector<Anomaly> detectAnomaliesIQR(const std::vector<TimeSeriesPoint>& data, double multiplier = 1.5) {
std::vector<double> values;
for (const auto& p : data) values.push_back(p.value);
double q1 = percentile(values, 25);
double q3 = percentile(values, 75);
double iqr = q3 - q1;
double lower = q1 - multiplier * iqr;
double upper = q3 + multiplier * iqr;
std::vector<Anomaly> anomalies;
for (size_t i = 0; i < data.size(); i++) {
if (data[i].value < lower || data[i].value > upper) {
double score = data[i].value < lower ? lower - data[i].value : data[i].value - upper;
anomalies.push_back({static_cast<int>(i), data[i].timestamp, data[i].value, "IQR", score});
}
}
return anomalies;
}
// --- Moving Averages ---
std::map<std::string, std::vector<double>> computeMovingAverages(
const std::vector<TimeSeriesPoint>& data, const std::vector<int>& windows = {7, 14, 30, 90}) {
int n = data.size();
std::vector<double> values(n);
for (int i = 0; i < n; i++) values[i] = data[i].value;
std::map<std::string, std::vector<double>> results;
for (int w : windows) {
if (w >= n) continue;
// SMA
std::vector<double> sma(n);
for (int i = 0; i < n; i++) {
int start = std::max(0, i - w + 1);
double sum = 0;
for (int j = start; j <= i; j++) sum += values[j];
sma[i] = sum / (i - start + 1);
}
results["SMA_" + std::to_string(w)] = sma;
// EMA
std::vector<double> ema(n);
double alpha = 2.0 / (w + 1);
ema[0] = values[0];
for (int i = 1; i < n; i++) {
ema[i] = alpha * values[i] + (1 - alpha) * ema[i - 1];
}
results["EMA_" + std::to_string(w)] = ema;
}
return results;
}
// --- Console Report ---
void printConsoleReport(const SummaryStats& stats, const TrendResult& trend,
const SeasonalResult& seasonal,
const std::vector<Anomaly>& zAnomalies,
const std::vector<Anomaly>& iqrAnomalies) {
std::cout << "\n" << std::string(70, '=') << "\n";
std::cout << " TIME SERIES TREND DETECTION REPORT\n";
std::cout << std::string(70, '=') << "\n";
std::cout << "\n--- Summary Statistics ---\n";
std::cout << " Data Points: " << stats.count << "\n";
std::cout << " Date Range: " << stats.startDate << " to " << stats.endDate << "\n";
std::cout << " Duration: " << stats.durationDays << " days\n";
std::cout << std::fixed << std::setprecision(4);
std::cout << " Mean: " << stats.mean << "\n";
std::cout << " Std Dev: " << stats.stddev << "\n";
std::cout << " Min: " << stats.minVal << "\n";
std::cout << " Max: " << stats.maxVal << "\n";
std::cout << " Median: " << stats.median << "\n";
std::cout << " Skewness: " << stats.skewness << "\n";
std::cout << " Kurtosis: " << stats.kurtosis << "\n";
std::cout << "\n--- Trend Analysis ---\n";
std::cout << " Direction: " << trend.direction << "\n";
std::cout << std::setprecision(6);
std::cout << " Linear Slope: " << trend.linearSlope << " per time step\n";
std::cout << std::setprecision(4);
std::cout << " Linear R^2: " << trend.linearRSquared << "\n";
std::cout << " Poly(2) R^2: " << trend.poly2RSquared << "\n";
std::cout << " Poly(3) R^2: " << trend.poly3RSquared << "\n";
std::cout << " Best Fit Degree: " << trend.bestPolynomialDegree << "\n";
std::cout << "\n--- Seasonal Decomposition ---\n";
std::cout << " Period: " << seasonal.period << "\n";
std::cout << " Strength: " << seasonal.seasonalStrength << "\n";
std::cout << " Amplitude: " << seasonal.seasonalAmplitude << "\n";
std::cout << " Residual Std: " << seasonal.residualStd << "\n";
std::cout << "\n--- Anomaly Detection ---\n";
std::cout << " Z-score anomalies: " << zAnomalies.size() << "\n";
int limit = std::min(5, static_cast<int>(zAnomalies.size()));
for (int i = 0; i < limit; i++) {
std::cout << std::setprecision(2);
std::cout << " " << zAnomalies[i].timestamp
<< " value=" << zAnomalies[i].value
<< " z=" << zAnomalies[i].score << "\n";
}
if (zAnomalies.size() > 5)
std::cout << " ... and " << (zAnomalies.size() - 5) << " more\n";
std::cout << " IQR anomalies: " << iqrAnomalies.size() << "\n";
limit = std::min(5, static_cast<int>(iqrAnomalies.size()));
for (int i = 0; i < limit; i++) {
std::cout << " " << iqrAnomalies[i].timestamp
<< " value=" << iqrAnomalies[i].value << "\n";
}
if (iqrAnomalies.size() > 5)
std::cout << " ... and " << (iqrAnomalies.size() - 5) << " more\n";
std::cout << "\n" << std::string(70, '=') << "\n";
}
// --- Save Report ---
void saveReport(const SummaryStats& stats, const TrendResult& trend,
const SeasonalResult& seasonal,
const std::vector<Anomaly>& zAnomalies,
const std::vector<Anomaly>& iqrAnomalies,
const std::string& outputPath) {
json report;
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::ostringstream oss;
oss << std::put_time(std::localtime(&time), "%Y-%m-%dT%H:%M:%S");
report["generated_at"] = oss.str();
report["summary_statistics"] = {
{"count", stats.count}, {"mean", stats.mean}, {"std", stats.stddev},
{"min", stats.minVal}, {"max", stats.maxVal}, {"median", stats.median},
{"q25", stats.q25}, {"q75", stats.q75}, {"skewness", stats.skewness},
{"kurtosis", stats.kurtosis}, {"start_date", stats.startDate},
{"end_date", stats.endDate}, {"duration_days", stats.durationDays}
};
report["trend_analysis"] = {
{"linear_slope", trend.linearSlope}, {"linear_intercept", trend.linearIntercept},
{"linear_r_squared", trend.linearRSquared}, {"poly2_r_squared", trend.poly2RSquared},
{"poly3_r_squared", trend.poly3RSquared}, {"best_polynomial_degree", trend.bestPolynomialDegree},
{"best_r_squared", trend.bestRSquared}, {"direction", trend.direction}
};
report["seasonal_decomposition"] = {
{"period", seasonal.period}, {"seasonal_strength", seasonal.seasonalStrength},
{"seasonal_amplitude", seasonal.seasonalAmplitude}, {"residual_std", seasonal.residualStd},
{"trend_component_mean", seasonal.trendComponentMean}
};
json zDetections = json::array();
for (const auto& a : zAnomalies) {
zDetections.push_back({{"index", a.index}, {"timestamp", a.timestamp},
{"value", a.value}, {"z_score", a.score}, {"method", a.method}});
}
json iqrDetections = json::array();
for (const auto& a : iqrAnomalies) {
iqrDetections.push_back({{"index", a.index}, {"timestamp", a.timestamp},
{"value", a.value}, {"score", a.score}, {"method", a.method}});
}
report["anomalies"] = {
{"z_score", {{"count", zAnomalies.size()}, {"threshold", 3.0}, {"detections", zDetections}}},
{"iqr", {{"count", iqrAnomalies.size()}, {"multiplier", 1.5}, {"detections", iqrDetections}}}
};
std::ofstream file(outputPath);
file << report.dump(2);
file.close();
std::cout << "Report saved to " << outputPath << std::endl;
}
// --- Main ---
int main(int argc, char* argv[]) {
std::string inputPath;
std::string outputDir = ".";
double zScoreThreshold = 3.0;
double iqrMultiplier = 1.5;
int seasonalPeriod = -1;
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if ((arg == "--input" || arg == "-i") && i + 1 < argc) inputPath = argv[++i];
else if ((arg == "--output" || arg == "-o") && i + 1 < argc) outputDir = argv[++i];
else if (arg == "--zscore-threshold" && i + 1 < argc) zScoreThreshold = std::stod(argv[++i]);
else if (arg == "--iqr-multiplier" && i + 1 < argc) iqrMultiplier = std::stod(argv[++i]);
else if ((arg == "--period" || arg == "-p") && i + 1 < argc) seasonalPeriod = std::stoi(argv[++i]);
}
if (inputPath.empty()) {
std::cout << "No input file specified. Generating sample data..." << std::endl;
inputPath = outputDir + "/sample_timeseries.csv";
generateSampleData(inputPath);
}
std::cout << "Loading data from " << inputPath << "..." << std::endl;
auto data = loadData(inputPath);
std::cout << "Loaded " << data.size() << " records." << std::endl;
data = handleMissingTimestamps(data);
std::cout << "After filling gaps: " << data.size() << " records." << std::endl;
auto stats = computeSummaryStats(data);
auto trend = detectTrend(data);
if (seasonalPeriod < 0) {
int n = data.size();
if (n >= 730) seasonalPeriod = 365;
else if (n >= 60) seasonalPeriod = 30;
else if (n >= 14) seasonalPeriod = 7;
else seasonalPeriod = std::max(2, n / 3);
}
if (seasonalPeriod >= static_cast<int>(data.size()) / 2) {
seasonalPeriod = std::max(2, static_cast<int>(data.size()) / 3);
}
auto seasonal = decomposeSeasons(data, seasonalPeriod);
auto zAnomalies = detectAnomaliesZScore(data, zScoreThreshold);
auto iqrAnomalies = detectAnomaliesIQR(data, iqrMultiplier);
auto movingAvgs = computeMovingAverages(data);
printConsoleReport(stats, trend, seasonal, zAnomalies, iqrAnomalies);
std::string reportPath = outputDir + "/trend_report.json";
saveReport(stats, trend, seasonal, zAnomalies, iqrAnomalies, reportPath);
std::cout << "\nAnalysis complete." << std::endl;
return 0;
}
README.md
# Time Series Trend Detector - C++ Trial 1 Analyzes time series data to detect trends, seasonal patterns, and anomalies using Eigen, nlohmann/json, and csv-parser. ## Dependencies - Eigen 3.4.0 - nlohmann/json 3.11.3 - csv-parser 2.3.0 ## Build & Run ```bash mkdir build && cd build cmake .. cmake --build . ./detector # With input file ./detector --input data.csv --output results/ ``` ## Output - Console report with summary statistics, trend analysis, seasonal decomposition, and anomaly detection - `trend_report.json` - Full analysis results in JSON format