← All tasks
javacodex/java-t1 #9Not a task: repair changed code

Time Series Trend Detector (java, written by Codex)

envgap__codex__java-t1-9

Written by a coding agent; not on GitHubWritten 2026-03-02

01 / FAILURE SIGNATURE

As the study recorded it

Compilation failure: local variable referenced from lambda must be final or effectively final
Not a benchmark task.
  • Its repair changed source code, so it is not an environment task.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
pom.xml
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/java-t1 #9 · read the task the agent was given
Codex wrote this java project from the task below. It does not run 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 Java project for a clean Ubuntu 22.04 machine with only JDK 17+ installed. Include:
- Source code
- pom.xml 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.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>tmlr.codex_generated.p09</groupId>
  <artifactId>time-series-trend-detector</artifactId>
  <version>1.0.0</version>
  <name>Time Series Trend Detector</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.release>17</maven.compiler.release>
  </properties>

  <dependencies>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.5.0</version>
        <configuration>
          <mainClass>TimeSeriesTrendDetector</mainClass>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
README.md
# Time Series Trend Detector (Java)

Analyzes CSV time series data for trend, seasonality, anomalies, and summary statistics.

## Requirements

- Ubuntu 22.04
- JDK 17+
- Maven 3.9+

## Dependencies

- Direct: none (Java standard library only)
- Transitive: none

Build plugin versions are pinned in `pom.xml` for reproducibility.

## Build

```bash
mvn -q -DskipTests compile
```

## Run

With input:

```bash
mvn -q exec:java -Dexec.args="/path/to/data.csv --window 7 --threshold 2.0 --output trend_report.json --export processed.csv"
```

No input (generates a 365-day sample and analyzes it):

```bash
mvn -q exec:java
```

## Features

- Timestamp parsing: ISO 8601, Unix epoch, `MM/DD/YYYY`, `YYYY-MM-DD HH:MM:SS`
- Moving average with configurable window
- Linear regression trend direction + slope + R-squared
- Autocorrelation-based seasonality period detection
- Anomaly detection against moving average using configurable std-dev threshold
- Gap interpolation and reporting
- JSON report and processed CSV export
src/main/java/TimeSeriesTrendDetector.java
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class TimeSeriesTrendDetector {
    private record ParsedArgs(Map<String, String> options, List<String> positional) {}
    private record Row(long timestampMs, String isoTimestamp, Map<String, Double> values, boolean gapFilled) {}
    private record Regression(double slope, double intercept, double r2) {}
    private record Seasonality(Integer period, Double autocorrelation) {}
    private record Analysis(
            String column,
            Map<String, Double> stats,
            Map<String, Object> trend,
            Map<String, Object> seasonality,
            List<Map<String, Object>> anomalies,
            List<Double> movingAverage
    ) {}

    public static void main(String[] args) {
        try {
            ParsedArgs parsed = parseArgs(args);
            int window = Math.max(1, Integer.parseInt(parsed.options.getOrDefault("window", "7")));
            double threshold = Double.parseDouble(parsed.options.getOrDefault("threshold", "2.0"));
            Path outputPath = Paths.get(parsed.options.getOrDefault("output", "trend_report.json")).toAbsolutePath();
            Path exportPath = parsed.options.containsKey("export")
                    ? Paths.get(parsed.options.get("export")).toAbsolutePath()
                    : null;

            Path inputPath;
            if (parsed.positional.isEmpty()) {
                inputPath = Paths.get("sample_timeseries.csv").toAbsolutePath();
                generateSample(inputPath);
                System.out.println("No input provided. Generated sample dataset: " + inputPath);
            } else {
                inputPath = Paths.get(parsed.positional.get(0)).toAbsolutePath();
                if (!Files.exists(inputPath)) {
                    System.err.println("Input file not found: " + inputPath);
                    System.exit(1);
                    return;
                }
            }

            ParseResult parsedInput = parseInputCsv(inputPath);
            if (parsedInput.rows.isEmpty() || parsedInput.valueColumns.isEmpty()) {
                System.err.println("No valid rows or value columns found.");
                System.exit(1);
                return;
            }

            GapResult gapResult = interpolateGaps(parsedInput.rows, parsedInput.valueColumns);
            List<Analysis> analyses = new ArrayList<>();
            for (String col : parsedInput.valueColumns) {
                analyses.add(analyzeColumn(gapResult.rows, col, window, threshold));
            }

            Map<String, Object> report = buildReport(inputPath, parsedInput.timestampColumn, parsedInput.valueColumns,
                    gapResult, analyses, window, threshold);
            Files.writeString(outputPath, toJson(report, 0) + "\n", StandardCharsets.UTF_8);
            if (exportPath != null) exportProcessedCsv(exportPath, gapResult.rows, analyses);
            printSummary(analyses, gapResult.gapPoints.size(), outputPath, exportPath);
        } catch (Exception e) {
            System.err.println("Failed: " + e.getMessage());
            System.exit(1);
        }
    }

    private record ParseResult(List<Row> rows, List<String> valueColumns, String timestampColumn) {}
    private record GapResult(List<Row> rows, long inferredStepMs, List<Long> gapPoints) {}

    private static ParsedArgs parseArgs(String[] args) {
        Map<String, String> options = new HashMap<>();
        List<String> positional = new ArrayList<>();
        for (int i = 0; i < args.length; i++) {
            String token = args[i];
            if (token.startsWith("--")) {
                String key = token.substring(2);
                if (i + 1 < args.length && !args[i + 1].startsWith("--")) options.put(key, args[++i]);
                else options.put(key, "true");
            } else {
                positional.add(token);
            }
        }
        return new ParsedArgs(options, positional);
    }

    private static List<String> parseCsvLine(String line) {
        List<String> out = new ArrayList<>();
        StringBuilder current = new StringBuilder();
        boolean inQuotes = false;
        for (int i = 0; i < line.length(); i++) {
            char ch = line.charAt(i);
            if (ch == '"') {
                if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') {
                    current.append('"');
                    i++;
                } else {
                    inQuotes = !inQuotes;
                }
            } else if (ch == ',' && !inQuotes) {
                out.add(current.toString());
                current.setLength(0);
            } else {
                current.append(ch);
            }
        }
        out.add(current.toString());
        return out;
    }

    private static Long parseTimestamp(String raw) {
        if (raw == null) return null;
        String s = raw.trim();
        if (s.isEmpty()) return null;

        if (s.matches("^\\d{10}$")) return Long.parseLong(s) * 1000L;
        if (s.matches("^\\d{13}$")) return Long.parseLong(s);

        if (s.matches("^\\d{1,2}/\\d{1,2}/\\d{4}$")) {
            DateTimeFormatter f = DateTimeFormatter.ofPattern("M/d/yyyy");
            LocalDate d = LocalDate.parse(s, f);
            return d.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
        }
        if (s.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$")) {
            DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            LocalDateTime dt = LocalDateTime.parse(s, f);
            return dt.toInstant(ZoneOffset.UTC).toEpochMilli();
        }

        try {
            return Instant.parse(s).toEpochMilli();
        } catch (DateTimeParseException ignored) {
        }

        try {
            return LocalDate.parse(s, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
        } catch (DateTimeParseException ignored) {
        }

        try {
            return LocalDateTime.parse(s, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toInstant(ZoneOffset.UTC).toEpochMilli();
        } catch (DateTimeParseException ignored) {
        }
        return null;
    }

    private static String iso(long tsMs) {
        return Instant.ofEpochMilli(tsMs).toString();
    }

    private static ParseResult parseInputCsv(Path path) throws IOException {
        List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
        lines.removeIf(s -> s.trim().isEmpty());
        if (lines.isEmpty()) return new ParseResult(new ArrayList<>(), new ArrayList<>(), "timestamp");
        if (!lines.get(0).isEmpty() && lines.get(0).charAt(0) == '\uFEFF') {
            lines.set(0, lines.get(0).substring(1));
        }

        List<String> headers = parseCsvLine(lines.get(0));
        if (headers.size() < 2) return new ParseResult(new ArrayList<>(), new ArrayList<>(), "timestamp");

        String tsCol = headers.get(0);
        List<String> valueCols = headers.subList(1, headers.size());
        List<Row> rows = new ArrayList<>();

        for (int i = 1; i < lines.size(); i++) {
            List<String> parts = parseCsvLine(lines.get(i));
            if (parts.isEmpty()) continue;
            Long ts = parseTimestamp(parts.get(0));
            if (ts == null) continue;
            Map<String, Double> values = new LinkedHashMap<>();
            for (int j = 0; j < valueCols.size(); j++) {
                String raw = j + 1 < parts.size() ? parts.get(j + 1).trim() : "";
                Double v = null;
                if (!raw.isEmpty()) {
                    try { v = Double.parseDouble(raw); } catch (NumberFormatException ignored) {}
                }
                values.put(valueCols.get(j), v);
            }
            rows.add(new Row(ts, iso(ts), values, false));
        }
        return new ParseResult(rows, new ArrayList<>(valueCols), tsCol);
    }

    private static void generateSample(Path path) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
            writer.write("timestamp,metric_a,metric_b\n");
            Instant start = Instant.parse("2025-01-01T00:00:00Z");
            for (int i = 0; i < 365; i++) {
                Instant ts = start.plusSeconds(86400L * i);
                double weekly = 10.0 * Math.sin((2.0 * Math.PI * i) / 7.0);
                double trend = i * 0.18;
                double a = 50 + trend + weekly + Math.sin(i) * 0.8;
                double b = 30 + i * 0.05 + 5 * Math.cos((2.0 * Math.PI * i) / 7.0);
                if (i == 45 || i == 123 || i == 251) a += 35;
                if (i == 200 || i == 300) b -= 20;
                writer.write(String.format("%s,%.3f,%.3f%n", ts.toString().substring(0, 10), a, b));
            }
        }
    }

    private static long inferStepMs(List<Row> rows) {
        List<Long> diffs = new ArrayList<>();
        for (int i = 1; i < rows.size(); i++) {
            long d = rows.get(i).timestampMs - rows.get(i - 1).timestampMs;
            if (d > 0) diffs.add(d);
        }
        if (diffs.isEmpty()) return 86400000L;
        Collections.sort(diffs);
        return diffs.get(diffs.size() / 2);
    }

    private static GapResult interpolateGaps(List<Row> rows, List<String> valueCols) {
        List<Row> ordered = new ArrayList<>(rows);
        ordered.sort((a, b) -> Long.compare(a.timestampMs, b.timestampMs));
        long step = inferStepMs(ordered);
        Map<Long, Row> byTs = new HashMap<>();
        for (Row r : ordered) byTs.put(r.timestampMs, r);
        long start = ordered.get(0).timestampMs;
        long end = ordered.get(ordered.size() - 1).timestampMs;
        List<Row> filled = new ArrayList<>();
        List<Long> gapPoints = new ArrayList<>();

        for (long t = start; t <= end; t += step) {
            if (byTs.containsKey(t)) {
                Row src = byTs.get(t);
                filled.add(new Row(src.timestampMs, src.isoTimestamp, new LinkedHashMap<>(src.values), false));
            } else {
                Map<String, Double> vals = new LinkedHashMap<>();
                for (String c : valueCols) vals.put(c, null);
                filled.add(new Row(t, iso(t), vals, true));
                gapPoints.add(t);
            }
        }

        for (String col : valueCols) {
            for (int i = 0; i < filled.size(); i++) {
                if (filled.get(i).values.get(col) != null) continue;
                int left = i - 1;
                while (left >= 0 && filled.get(left).values.get(col) == null) left--;
                int right = i + 1;
                while (right < filled.size() && filled.get(right).values.get(col) == null) right++;
                if (left >= 0 && right < filled.size()) {
                    double lv = filled.get(left).values.get(col);
                    double rv = filled.get(right).values.get(col);
                    double ratio = (double) (filled.get(i).timestampMs - filled.get(left).timestampMs) /
                            (double) (filled.get(right).timestampMs - filled.get(left).timestampMs);
                    double v = lv + (rv - lv) * ratio;
                    filled.get(i).values.put(col, v);
                }
            }
        }
        return new GapResult(filled, step, gapPoints);
    }

    private static Double mean(List<Double> values) {
        if (values.isEmpty()) return null;
        double sum = 0.0;
        for (Double v : values) sum += v;
        return sum / values.size();
    }

    private static Double variance(List<Double> values, Double m) {
        if (values.isEmpty()) return null;
        double mu = (m == null ? mean(values) : m);
        if (mu == 0 && m == null && values.isEmpty()) return null;
        double acc = 0.0;
        for (Double v : values) acc += (v - mu) * (v - mu);
        return acc / values.size();
    }

    private static Double stddev(List<Double> values, Double m) {
        Double v = variance(values, m);
        return v == null ? null : Math.sqrt(v);
    }

    private static List<Double> movingAverage(List<Double> values, int window) {
        List<Double> out = new ArrayList<>(Collections.nCopies(values.size(), null));
        for (int i = 0; i < values.size(); i++) {
            int start = Math.max(0, i - window + 1);
            List<Double> chunk = new ArrayList<>();
            for (int j = start; j <= i; j++) if (values.get(j) != null) chunk.add(values.get(j));
            out.set(i, chunk.isEmpty() ? null : mean(chunk));
        }
        return out;
    }

    private static Regression linearRegression(List<Double> values) {
        List<Integer> xs = new ArrayList<>();
        List<Double> ys = new ArrayList<>();
        for (int i = 0; i < values.size(); i++) {
            if (values.get(i) != null) {
                xs.add(i);
                ys.add(values.get(i));
            }
        }
        if (ys.size() < 2) return new Regression(0.0, 0.0, 0.0);
        double mx = mean(xs.stream().map(Double::valueOf).toList());
        double my = mean(ys);
        double cov = 0.0;
        double vx = 0.0;
        for (int i = 0; i < ys.size(); i++) {
            cov += (xs.get(i) - mx) * (ys.get(i) - my);
            vx += (xs.get(i) - mx) * (xs.get(i) - mx);
        }
        double slope = vx == 0 ? 0 : cov / vx;
        double intercept = my - slope * mx;
        double ssRes = 0.0;
        double ssTot = 0.0;
        for (int i = 0; i < ys.size(); i++) {
            double pred = slope * xs.get(i) + intercept;
            ssRes += (ys.get(i) - pred) * (ys.get(i) - pred);
            ssTot += (ys.get(i) - my) * (ys.get(i) - my);
        }
        double r2 = ssTot == 0 ? 0 : 1.0 - (ssRes / ssTot);
        return new Regression(slope, intercept, r2);
    }

    private static Double autocorrelation(List<Double> values, int lag) {
        List<Double> xs = new ArrayList<>();
        List<Double> ys = new ArrayList<>();
        for (int i = lag; i < values.size(); i++) {
            Double a = values.get(i);
            Double b = values.get(i - lag);
            if (a == null || b == null) continue;
            xs.add(a);
            ys.add(b);
        }
        if (xs.size() < 3) return null;
        double mx = mean(xs);
        double my = mean(ys);
        double num = 0.0;
        double dx = 0.0;
        double dy = 0.0;
        for (int i = 0; i < xs.size(); i++) {
            num += (xs.get(i) - mx) * (ys.get(i) - my);
            dx += (xs.get(i) - mx) * (xs.get(i) - mx);
            dy += (ys.get(i) - my) * (ys.get(i) - my);
        }
        double den = Math.sqrt(dx * dy);
        return den == 0 ? 0.0 : num / den;
    }

    private static Seasonality detectSeasonality(List<Double> values) {
        int maxLag = Math.min(60, values.size() / 2);
        Integer bestLag = null;
        double bestCorr = 0.0;
        for (int lag = 2; lag <= maxLag; lag++) {
            Double corr = autocorrelation(values, lag);
            if (corr == null) continue;
            if (Math.abs(corr) > Math.abs(bestCorr)) {
                bestCorr = corr;
                bestLag = lag;
            }
        }
        if (bestLag == null || Math.abs(bestCorr) < 0.3) return new Seasonality(null, null);
        return new Seasonality(bestLag, bestCorr);
    }

    private static Analysis analyzeColumn(List<Row> rows, String col, int window, double threshold) {
        List<Double> values = new ArrayList<>();
        for (Row r : rows) values.add(r.values.get(col));
        List<Double> ma = movingAverage(values, window);
        List<Double> valid = values.stream().filter(v -> v != null).toList();
        Double mu = mean(valid);
        Double var = variance(valid, mu);
        Double sd = stddev(valid, mu);
        Regression reg = linearRegression(values);
        Seasonality season = detectSeasonality(values);

        String direction = "stable";
        double scale = (sd == null || sd == 0.0) ? 1.0 : sd;
        if (Math.abs(reg.slope) >= scale * 0.001) direction = reg.slope > 0 ? "increasing" : "decreasing";

        List<Map<String, Object>> anomalies = new ArrayList<>();
        for (int i = 0; i < values.size(); i++) {
            Double v = values.get(i);
            Double m = ma.get(i);
            if (v == null || m == null || sd == null || sd == 0.0) continue;
            double z = Math.abs(v - m) / sd;
            if (z > threshold) {
                Map<String, Object> an = new LinkedHashMap<>();
                an.put("index", i);
                an.put("timestamp", rows.get(i).isoTimestamp);
                an.put("value", v);
                an.put("moving_average", m);
                an.put("z_from_moving_average", z);
                anomalies.add(an);
            }
        }

        Map<String, Double> stats = new LinkedHashMap<>();
        stats.put("min", valid.isEmpty() ? null : Collections.min(valid));
        stats.put("max", valid.isEmpty() ? null : Collections.max(valid));
        stats.put("mean", mu);
        stats.put("variance", var);
        stats.put("stddev", sd);

        Map<String, Object> trend = new LinkedHashMap<>();
        trend.put("direction", direction);
        trend.put("slope", reg.slope);
        trend.put("r_squared", reg.r2);

        Map<String, Object> seasonality = new LinkedHashMap<>();
        seasonality.put("period", season.period);
        seasonality.put("autocorrelation", season.autocorrelation);

        return new Analysis(col, stats, trend, seasonality, anomalies, ma);
    }

    private static Map<String, Object> buildReport(Path inputPath, String timestampColumn, List<String> valueColumns,
                                                   GapResult gapResult, List<Analysis> analyses, int window, double threshold) {
        Map<String, Object> metadata = new LinkedHashMap<>();
        metadata.put("input_file", inputPath.toString());
        metadata.put("generated_at", Instant.now().toString());
        metadata.put("window", window);
        metadata.put("threshold", threshold);
        metadata.put("inferred_step_millis", gapResult.inferredStepMs);
        List<String> gapIso = gapResult.gapPoints.stream().map(TimeSeriesTrendDetector::iso).toList();
        metadata.put("interpolated_gap_points", gapIso);

        List<Map<String, Object>> analysisOut = new ArrayList<>();
        for (Analysis a : analyses) {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("column", a.column);
            item.put("stats", a.stats);
            item.put("trend", a.trend);
            item.put("seasonality", a.seasonality);
            item.put("anomaly_count", a.anomalies.size());

            List<Map<String, Object>> anOut = new ArrayList<>();
            for (Map<String, Object> an : a.anomalies) {
                Map<String, Object> mapped = new LinkedHashMap<>();
                mapped.put("timestamp", an.get("timestamp"));
                mapped.put("value", an.get("value"));
                mapped.put("moving_average", an.get("moving_average"));
                mapped.put("z_from_moving_average", an.get("z_from_moving_average"));
                anOut.add(mapped);
            }
            item.put("anomalies", anOut);
            analysisOut.add(item);
        }

        Map<String, Object> report = new LinkedHashMap<>();
        report.put("metadata", metadata);
        report.put("timestamp_column", timestampColumn);
        report.put("value_columns", valueColumns);
        report.put("analyses", analysisOut);
        return report;
    }

    private static void exportProcessedCsv(Path path, List<Row> rows, List<Analysis> analyses) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
            List<String> header = new ArrayList<>();
            header.add("timestamp");
            for (Analysis a : analyses) {
                header.add(a.column);
                header.add(a.column + "_moving_average");
                header.add(a.column + "_is_anomaly");
            }
            writer.write(String.join(",", header));
            writer.write("\n");

            for (int i = 0; i < rows.size(); i++) {
                List<String> line = new ArrayList<>();
                line.add(csvEscape(rows.get(i).isoTimestamp));
                for (Analysis a : analyses) {
                    Double value = rows.get(i).values.get(a.column);
                    Double ma = a.movingAverage.get(i);
                    boolean isAnomaly = a.anomalies.stream().anyMatch(x -> ((Number) x.get("index")).intValue() == i);
                    line.add(csvEscape(value));
                    line.add(csvEscape(ma));
                    line.add(isAnomaly ? "1" : "0");
                }
                writer.write(String.join(",", line));
                writer.write("\n");
            }
        }
    }

    private static String csvEscape(Object value) {
        if (value == null) return "";
        String s = String.valueOf(value);
        if (s.contains(",") || s.contains("\"") || s.contains("\n")) return "\"" + s.replace("\"", "\"\"") + "\"";
        return s;
    }

    private static void printSummary(List<Analysis> analyses, int gapCount, Path outputPath, Path exportPath) {
        System.out.println("Time Series Trend Detector");
        System.out.println("==========================");
        System.out.println("Columns analyzed: " + analyses.size());
        System.out.println("Gap points filled/interpolated: " + gapCount);
        System.out.println();
        for (Analysis a : analyses) {
            System.out.println("Column: " + a.column);
            System.out.printf("  Trend      : %s (slope=%.6f, R^2=%.4f)%n",
                    a.trend.get("direction"), (double) a.trend.get("slope"), (double) a.trend.get("r_squared"));
            if (a.seasonality.get("period") == null) {
                System.out.println("  Seasonality: none");
            } else {
                System.out.printf("  Seasonality: period=%s (autocorr=%.4f)%n",
                        a.seasonality.get("period"), (double) a.seasonality.get("autocorrelation"));
            }
            System.out.println("  Anomalies  : " + a.anomalies.size());
            System.out.printf("  Stats      : min=%s, max=%s, mean=%s, variance=%s%n",
                    a.stats.get("min"), a.stats.get("max"), a.stats.get("mean"), a.stats.get("variance"));
            System.out.println();
        }
        System.out.println("JSON report saved : " + outputPath);
        if (exportPath != null) System.out.println("Processed CSV saved: " + exportPath);
    }

    private static String toJson(Object obj, int indent) {
        String pad = "  ".repeat(indent);
        if (obj == null) return "null";
        if (obj instanceof String s) return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
        if (obj instanceof Number || obj instanceof Boolean) return String.valueOf(obj);
        if (obj instanceof Map<?, ?> map) {
            StringBuilder out = new StringBuilder();
            out.append("{\n");
            int i = 0;
            for (Map.Entry<?, ?> e : map.entrySet()) {
                out.append(pad).append("  ").append(toJson(String.valueOf(e.getKey()), 0)).append(": ")
                        .append(toJson(e.getValue(), indent + 1));
                if (++i < map.size()) out.append(",");
                out.append("\n");
            }
            out.append(pad).append("}");
            return out.toString();
        }
        if (obj instanceof List<?> list) {
            StringBuilder out = new StringBuilder();
            out.append("[\n");
            for (int i = 0; i < list.size(); i++) {
                out.append(pad).append("  ").append(toJson(list.get(i), indent + 1));
                if (i + 1 < list.size()) out.append(",");
                out.append("\n");
            }
            out.append(pad).append("]");
            return out.toString();
        }
        return toJson(String.valueOf(obj), indent);
    }
}