diff --git a/playground/index.html b/playground/index.html index 48bfbcb9..bf6823ba 100644 --- a/playground/index.html +++ b/playground/index.html @@ -264,6 +264,86 @@

βœ… Complete +
+

πŸ“₯ insertColumn / popColumn

+

Insert and remove DataFrame columns at precise positions. insertColumn(df, loc, col, values) inserts at integer position, popColumn(df, col) returns { series, df }. Also includes reorderColumns and moveColumn. Mirrors pandas.DataFrame.insert() and .pop().

+
βœ… Complete
+
+
+

βœ‚οΈ cut / qcut

+

Bin continuous numeric data into discrete intervals. cut() uses fixed-width or explicit bin edges; qcut() uses quantile-based bins of equal population. Both return codes, labels, and bin edges. Mirrors pandas.cut and pandas.qcut.

+
βœ… Complete
+
+
+

πŸ“Š Rolling Extended Stats

+

Higher-order rolling window statistics: rollingSem (standard error of mean), rollingSkew (Fisher-Pearson skewness), rollingKurt (excess kurtosis), and rollingQuantile (arbitrary percentile with 5 interpolation methods). Mirrors pandas.Series.rolling().sem/skew/kurt/quantile().

+
βœ… Complete
+
+
+

πŸ”§ Rolling Apply & Multi-Agg

+

Standalone custom rolling-window functions: rollingApply (custom fn per window), rollingAgg (multiple named aggregations β†’ DataFrame), dataFrameRollingApply, dataFrameRollingAgg. Supports minPeriods, center, and raw mode. Mirrors pandas.Rolling.apply() and Rolling.agg().

+
βœ… Complete
+
+
+

🎭 where / mask

+

Element-wise conditional selection: seriesWhere / seriesMask and dataFrameWhere / dataFrameMask. Accepts boolean arrays, label-aligned boolean Series/DataFrame, or callables. Mirrors pandas.Series.where, pandas.DataFrame.where, and their .mask() inverses.

+
βœ… Complete
+
+
+

πŸ” isna / notna

+

Module-level missing-value detection: isna, notna, isnull, notnull work on scalars, arrays, Series, and DataFrames. Plus standalone fillna, dropna, countna, and countValid. Mirrors pandas.isna, pandas.notna, pandas.isnull, pandas.notnull.

+
βœ… Complete
+
+
+

🏷️ attrs β€” User Metadata

+

Attach arbitrary key→value metadata to any Series or DataFrame via a WeakMap registry. Provides getAttrs, setAttrs, updateAttrs, copyAttrs, withAttrs, mergeAttrs, clearAttrs, getAttr, setAttr, deleteAttr, attrsCount, attrsKeys. Mirrors pandas.DataFrame.attrs / pandas.Series.attrs.

+
βœ… Complete
+
+
+

πŸ”€ string_ops β€” Standalone String Ops

+

Module-level string utilities: strNormalize (Unicode NFC/NFD/NFKC/NFKD), strGetDummies (one-hot DataFrame), strExtractAll (all regex matches), strRemovePrefix, strRemoveSuffix, strTranslate (char-level substitution), strCharWidth (CJK-aware display width), strByteLength. Works on Series, arrays, or scalars.

+
βœ… Complete
+
+
+

πŸ”€ string_ops_extended β€” Extended String Ops

+

Advanced string utilities: strSplitExpand (split β†’ DataFrame columns), strExtractGroups (regex capture groups β†’ DataFrame), strPartition / strRPartition (split into before/sep/after), strMultiReplace (batch replacements), strIndent / strDedent (line-level indentation). Works on Series, arrays, or scalars.

+
βœ… Complete
+
+
+

πŸ”— pipe_apply β€” Pipeline & Apply Utilities

+

Standalone equivalents of pandas' pipe() / apply() / applymap(): pipe (variadic type-safe pipeline), seriesApply (element-wise with label/pos context), seriesTransform, dataFrameApply (axis 0/1), dataFrameApplyMap (cell-wise), dataFrameTransform (column-wise), dataFrameTransformRows (row-wise).

+
βœ… Complete
+
+
+

πŸ”’ numeric_extended β€” Numeric Utilities

+

numpy/scipy-style numeric utilities: digitize (bin values), histogram (frequency counts with density option), linspace / arange (number sequences), percentileOfScore (percentile rank of a score), zscore (z-score standardisation), minMaxNormalize (scale to [0,1] or custom range), coefficientOfVariation (std/mean). Series-aware variants included.

+
βœ… Complete
+
+ +
+
+

🏷️ categorical_ops β€” Categorical Utilities

+

Standalone categorical helpers: catFromCodes (from integer codes), set operations (catUnionCategories, catIntersectCategories, catDiffCategories, catEqualCategories), catSortByFreq, catToOrdinal, catFreqTable, catCrossTab, catRecode.

+
βœ… Complete
+
+
+
+
+

πŸ”’ format_ops β€” Number Formatting

+

Number-formatting helpers for Series and DataFrame. Scalar formatters: formatFloat, formatPercent, formatScientific, formatEngineering, formatThousands, formatCurrency, formatCompact. Formatter factories: makeFloatFormatter, makePercentFormatter, makeCurrencyFormatter. Apply to collections: applySeriesFormatter, applyDataFrameFormatter. Render to string: seriesToString, dataFrameToString.

+
βœ… Complete
+
+
+ + +
+

Performance

+
+
+

⚑ Benchmarks

+

Side-by-side performance comparison of tsb (TypeScript/Bun) vs pandas (Python). Timing metrics for each function.

+
πŸ—οΈ In Progress
+
diff --git a/playground/pct_change.html b/playground/pct_change.html new file mode 100644 index 00000000..ec1b4e3b --- /dev/null +++ b/playground/pct_change.html @@ -0,0 +1,452 @@ + + + + + + tsb β€” pct_change + + + +
+
+
Initializing playground…
+
+ ← Back to roadmap +

πŸ“Š pct_change β€” Interactive Playground

+

Compute the fractional change between each element and a prior element. + Mirrors pandas.Series.pct_change() / + pandas.DataFrame.pct_change().
+ Edit any code block below and press β–Ά Run + (or Ctrl+Enter) to execute it live in your browser. +

+ + +
+

1 Β· Basic pct_change on a Series

+

pctChangeSeries(series) returns the fractional (not percentage) change + from each previous element. The first element is always null.

+
+
+ TypeScript +
+ + +
+
+ + +
Click β–Ά Run to execute
+
Ctrl+Enter to run Β· Tab to indent
+
+
+ + +
+

2 Β· Multi-period change

+

The periods option controls the lag. Use periods: 2 to + compare each value to the one two steps earlier β€” useful for month-over-month + comparisons in quarterly data.

+
+
+ TypeScript +
+ + +
+
+ + +
Click β–Ά Run to execute
+
Ctrl+Enter to run Β· Tab to indent
+
+
+ + +
+

3 Β· Handling missing values

+

By default, pctChangeSeries forward-fills (fillMethod: "pad") + NaN/null values before computing the ratio β€” so gaps don't break the chain. + Set fillMethod: null to propagate NaN instead.

+
+
+ TypeScript +
+ + +
+
+ + +
Click β–Ά Run to execute
+
Ctrl+Enter to run Β· Tab to indent
+
+
+ + +
+

4 Β· Limit consecutive fills

+

The limit option caps how many consecutive NaN values get forward-filled. + Useful when you want to tolerate short gaps but not bridge large ones.

+
+
+ TypeScript +
+ + +
+
+ + +
Click β–Ά Run to execute
+
Ctrl+Enter to run Β· Tab to indent
+
+
+ + +
+

5 Β· DataFrame column-wise pct_change

+

pctChangeDataFrame(df) applies pctChangeSeries to every + column independently. Ideal for comparing multiple assets or metrics simultaneously.

+
+
+ TypeScript +
+ + +
+
+ + +
Click β–Ά Run to execute
+
Ctrl+Enter to run Β· Tab to indent
+
+
+ + +
+

6 Β· Negative periods (look-forward change)

+

A negative periods value computes the forward change: how much will + this element change by the time we reach |periods| steps ahead. + Useful for computing returns on a "hold for N periods" strategy.

+
+
+ TypeScript +
+ + +
+
+ + +
Click β–Ά Run to execute
+
Ctrl+Enter to run Β· Tab to indent
+
+
+ + +
+

API Reference

+

All functions return a new Series/DataFrame of the same shape β€” inputs are never mutated.

+
// Series
+pctChangeSeries(series, {
+  periods?: number,           // default 1 (positive = look back, negative = look forward)
+  fillMethod?: "pad" | "bfill" | null,  // default "pad"
+  limit?: number | null,      // max consecutive fills; default unlimited
+}): Series
+
+// DataFrame
+pctChangeDataFrame(df, {
+  periods?: number,
+  fillMethod?: "pad" | "bfill" | null,
+  limit?: number | null,
+  axis?: 0 | 1 | "index" | "columns",  // default 0 (column-wise)
+}): DataFrame
+
+ + + + + diff --git a/src/index.ts b/src/index.ts index 1dd0aa57..37c6e62e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ * * @packageDocumentation */ +// merged: 2026-04-09T19:37Z (re-merge main into PR branch, barrel-export conflicts resolved by keeping PR superset) // Core exports will be added here as features are implemented. // Each module is imported and re-exported from its feature file in src/. @@ -45,12 +46,16 @@ export { DatetimeAccessor } from "./core/index.ts"; export type { DatetimeSeriesLike } from "./core/index.ts"; export { DataFrameGroupBy, SeriesGroupBy } from "./groupby/index.ts"; export type { AggFn, AggName, AggSpec } from "./groupby/index.ts"; +export { NamedAgg, namedAgg, isNamedAggSpec } from "./groupby/index.ts"; +export type { NamedAggSpec } from "./groupby/index.ts"; export { describe, quantile } from "./stats/index.ts"; export type { DescribeOptions } from "./stats/index.ts"; export { readCsv, toCsv } from "./io/index.ts"; export type { ReadCsvOptions, ToCsvOptions } from "./io/index.ts"; export { readJson, toJson } from "./io/index.ts"; export type { ReadJsonOptions, ToJsonOptions, JsonOrient } from "./io/index.ts"; +export { jsonNormalize } from "./io/index.ts"; +export type { JsonNormalizeOptions, JsonPath } from "./io/index.ts"; export { pearsonCorr, dataFrameCorr, dataFrameCov } from "./stats/index.ts"; export type { CorrMethod, CorrOptions, CovOptions } from "./stats/index.ts"; export { Rolling } from "./window/index.ts"; @@ -61,6 +66,13 @@ export type { ExpandingOptions, ExpandingSeriesLike } from "./window/index.ts"; export { DataFrameExpanding } from "./core/index.ts"; export { EWM } from "./window/index.ts"; export type { EwmOptions, EwmSeriesLike } from "./window/index.ts"; +export { + rollingApply, + rollingAgg, + dataFrameRollingApply, + dataFrameRollingAgg, +} from "./window/index.ts"; +export type { RollingApplyOptions, RollingAggOptions, AggFunctions } from "./window/index.ts"; export { DataFrameEwm } from "./core/index.ts"; export { CategoricalAccessor } from "./core/index.ts"; export type { CatSeriesLike } from "./core/index.ts"; @@ -74,6 +86,10 @@ export type { } from "./reshape/index.ts"; export { stack, unstack, STACK_DEFAULT_SEP } from "./reshape/index.ts"; export type { StackOptions, UnstackOptions } from "./reshape/index.ts"; +export { wideToLong } from "./reshape/index.ts"; +export type { WideToLongOptions } from "./reshape/index.ts"; +export { pivotTableFull } from "./reshape/index.ts"; +export type { PivotTableFullOptions } from "./reshape/index.ts"; export { MultiIndex } from "./core/index.ts"; export type { MultiIndexOptions } from "./core/index.ts"; export { rankSeries, rankDataFrame } from "./stats/index.ts"; @@ -107,3 +123,415 @@ export { export type { ClipOptions, RoundOptions, DataFrameElemOptions } from "./stats/index.ts"; export { valueCounts, dataFrameValueCounts } from "./stats/index.ts"; export type { ValueCountsOptions, DataFrameValueCountsOptions } from "./stats/index.ts"; +export { whereSeries, maskSeries, whereDataFrame, maskDataFrame } from "./stats/index.ts"; +export type { + WherePredicate, + SeriesCond, + DataFrameCond, + WhereMaskOptions, +} from "./stats/index.ts"; +export { + seriesEq, + seriesNe, + seriesLt, + seriesGt, + seriesLe, + seriesGe, + dataFrameEq, + dataFrameNe, + dataFrameLt, + dataFrameGt, + dataFrameLe, + dataFrameGe, +} from "./stats/index.ts"; +export type { CompareOp, SeriesOther, DataFrameOther } from "./stats/index.ts"; +export { shiftSeries, diffSeries, dataFrameShift, dataFrameDiff } from "./stats/index.ts"; +export type { ShiftDiffDataFrameOptions } from "./stats/index.ts"; +export { interpolateSeries, dataFrameInterpolate } from "./stats/index.ts"; +export type { + InterpolateMethod, + LimitDirection, + InterpolateOptions, + DataFrameInterpolateOptions, +} from "./stats/index.ts"; +export { fillnaSeries, fillnaDataFrame } from "./stats/index.ts"; +export type { + FillnaMethod, + FillnaSeriesOptions, + ColumnFillMap, + FillnaDataFrameOptions, +} from "./stats/index.ts"; +export { Interval, IntervalIndex } from "./core/index.ts"; +export type { IntervalClosed, IntervalIndexOptions } from "./core/index.ts"; +export { cut, qcut, cutIntervalIndex, qcutIntervalIndex } from "./stats/index.ts"; +export type { CutOptions, QCutOptions } from "./stats/index.ts"; +export { sampleSeries, sampleDataFrame } from "./stats/index.ts"; +export type { SampleSeriesOptions, SampleDataFrameOptions } from "./stats/index.ts"; +export { applySeries, applymap, dataFrameApply } from "./stats/index.ts"; +export type { DataFrameApplyOptions } from "./stats/index.ts"; +export { CategoricalIndex } from "./core/index.ts"; +export type { CategoricalIndexOptions } from "./core/index.ts"; +export { + pipeSeries, + dataFramePipe, + pipeTo, + dataFramePipeTo, + pipeChain, + dataFramePipeChain, +} from "./stats/index.ts"; + +export { Period, PeriodIndex } from "./core/index.ts"; +export type { PeriodFreq, PeriodIndexOptions } from "./core/index.ts"; +export { Timedelta, TimedeltaIndex } from "./core/index.ts"; +export type { TimedeltaComponents, TimedeltaIndexOptions } from "./core/index.ts"; +export { + Day, + Hour, + Minute, + Second, + Milli, + Week, + MonthEnd, + MonthBegin, + YearEnd, + YearBegin, + BusinessDay, +} from "./core/index.ts"; +export type { DateOffset, WeekOptions } from "./core/index.ts"; +export { DatetimeIndex, date_range, bdate_range, resolveFreq } from "./core/index.ts"; +export type { DateRangeFreq, DateRangeOptions, DatetimeIndexOptions } from "./core/index.ts"; +export { TZDatetimeIndex, tz_localize, tz_convert } from "./core/index.ts"; +export { + seriesFloor, + dataFrameFloor, + seriesCeil, + dataFrameCeil, + seriesTrunc, + dataFrameTrunc, + seriesSqrt, + dataFrameSqrt, + seriesExp, + dataFrameExp, + seriesLog, + dataFrameLog, + seriesLog2, + dataFrameLog2, + seriesLog10, + dataFrameLog10, + seriesSign, + dataFrameSign, +} from "./stats/index.ts"; +export { + seriesPow, + dataFramePow, + seriesMod, + dataFrameMod, + seriesFloorDiv, + dataFrameFloorDiv, +} from "./stats/index.ts"; +export { + seriesAdd, + seriesRadd, + seriesSub, + seriesRsub, + seriesMul, + seriesRmul, + seriesDiv, + seriesRdiv, + dataFrameAdd, + dataFrameRadd, + dataFrameSub, + dataFrameRsub, + dataFrameMul, + dataFrameRmul, + dataFrameDiv, + dataFrameRdiv, +} from "./stats/index.ts"; +export { getDummies, dataFrameGetDummies } from "./stats/index.ts"; +export type { GetDummiesOptions, DataFrameGetDummiesOptions } from "./stats/index.ts"; +export { factorize, seriesFactorize } from "./stats/index.ts"; +export type { FactorizeOptions, FactorizeResult } from "./stats/index.ts"; +export { crosstab, seriesCrosstab } from "./stats/index.ts"; +export type { AggFunc, Normalize, CrosstabOptions } from "./stats/index.ts"; +export { toNumeric, toNumericArray, toNumericScalar, toNumericSeries } from "./stats/index.ts"; +export type { ToNumericDowncast, ToNumericErrors, ToNumericOptions } from "./stats/index.ts"; +export { seriesMemoryUsage, dataFrameMemoryUsage } from "./stats/index.ts"; +export type { MemoryUsageOptions } from "./stats/index.ts"; +export { selectDtypes } from "./stats/index.ts"; +export type { DtypeSelector, SelectDtypesOptions } from "./stats/index.ts"; +export { clipSeriesWithBounds, clipDataFrameWithBounds } from "./stats/index.ts"; +export type { + BoundArg, + SeriesClipBoundsOptions, + DataFrameClipBoundsOptions, +} from "./stats/index.ts"; +export { Timestamp } from "./core/index.ts"; +export type { TimestampOptions, TimestampComponents, TimestampUnit } from "./core/index.ts"; +export { dataFrameAssign } from "./core/index.ts"; +export type { AssignColSpec, AssignSpec } from "./core/index.ts"; +export { inferDtype } from "./stats/index.ts"; +export type { InferredDtype, InferDtypeOptions } from "./stats/index.ts"; +export { isna, notna, isnull, notnull } from "./stats/index.ts"; +export { dropna, dropnaSeries, dropnaDataFrame } from "./stats/index.ts"; +export type { DropnaHow, DropnaDataFrameOptions } from "./stats/index.ts"; +export { combineFirstSeries, combineFirstDataFrame } from "./stats/index.ts"; +export { natCompare, natSorted, natSortKey, natArgSort } from "./core/index.ts"; +export type { NatSortOptions, NatSortedOptions } from "./core/index.ts"; +export { searchsorted, searchsortedMany, argsortScalars } from "./core/index.ts"; +export type { SearchSortedSide, SearchSortedOptions } from "./core/index.ts"; +export { valueCountsBinned } from "./stats/index.ts"; +export type { ValueCountsBinnedOptions } from "./stats/index.ts"; + +export { + duplicatedSeries, + duplicatedDataFrame, + dropDuplicatesSeries, + dropDuplicatesDataFrame, +} from "./stats/index.ts"; +export type { + KeepPolicy, + DuplicatedDataFrameOptions, + DuplicatedSeriesOptions, +} from "./stats/index.ts"; +export { reindexSeries, reindexDataFrame } from "./core/index.ts"; +export type { ReindexMethod, ReindexSeriesOptions, ReindexDataFrameOptions } from "./core/index.ts"; + +export { alignSeries, alignDataFrame } from "./core/index.ts"; +export type { AlignSeriesOptions, AlignDataFrameOptions } from "./core/index.ts"; + +export { explodeSeries, explodeDataFrame } from "./stats/index.ts"; +export type { ExplodeOptions, ExplodeDataFrameOptions } from "./stats/index.ts"; + +export { isin, dataFrameIsin } from "./stats/index.ts"; +export type { IsinValues, IsinDict, DataFrameIsinValues } from "./stats/index.ts"; + +export { + insertColumn, + popColumn, + reorderColumns, + moveColumn, + dataFrameFromPairs, +} from "./core/index.ts"; +export type { PopResult } from "./core/index.ts"; +export { toDictOriented, fromDictOriented } from "./core/index.ts"; +export type { + ToDictOrient, + FromDictOrient, + DictSplit, + DictTight, + SplitInput, +} from "./core/index.ts"; +export { rollingSem, rollingSkew, rollingKurt, rollingQuantile } from "./stats/index.ts"; +export type { WindowExtOptions, RollingQuantileOptions } from "./stats/index.ts"; +export { fillna, countna, countValid } from "./stats/index.ts"; +export type { IsnaInput, FillnaOptions, DropnaOptions } from "./stats/index.ts"; +export { + getAttrs, + setAttrs, + updateAttrs, + copyAttrs, + withAttrs, + clearAttrs, + hasAttrs, + getAttr, + setAttr, + deleteAttr, + attrsCount, + attrsKeys, + mergeAttrs, +} from "./core/index.ts"; +export type { Attrs } from "./core/index.ts"; +export { + pipe, + seriesApply, + seriesTransform, + dataFrameApplyMap, + dataFrameTransform, + dataFrameTransformRows, +} from "./core/index.ts"; +export { + isScalar, + isListLike, + isArrayLike, + isDictLike, + isIterator, + isNumber, + isBool, + isStringValue, + isFloat, + isInteger, + isBigInt, + isRegExp, + isReCompilable, + isMissing, + isHashable, + isDate, + isNumericDtype, + isIntegerDtype, + isSignedIntegerDtype, + isUnsignedIntegerDtype, + isFloatDtype, + isBoolDtype, + isStringDtype, + isDatetimeDtype, + isTimedeltaDtype, + isCategoricalDtype, + isObjectDtype, + isComplexDtype, + isExtensionArrayDtype, + isPeriodDtype, + isIntervalDtype, +} from "./core/index.ts"; +export { + strNormalize, + strGetDummies, + strExtractAll, + strRemovePrefix, + strRemoveSuffix, + strTranslate, + strCharWidth, + strByteLength, + strSplitExpand, + strExtractGroups, + strPartition, + strRPartition, + strMultiReplace, + strIndent, + strDedent, +} from "./stats/index.ts"; +export type { + NormalizeForm, + StrInput, + ExtractAllOptions, + SplitExpandOptions, + ExtractGroupsOptions, + PartitionResult, + ReplacePair, + IndentOptions, +} from "./stats/index.ts"; +export { + digitize, + histogram, + linspace, + arange, + percentileOfScore, + zscore, + minMaxNormalize, + coefficientOfVariation, + seriesDigitize, +} from "./stats/index.ts"; +export type { + HistogramOptions, + HistogramResult, + ZscoreOptions, + MinMaxOptions, + CvOptions, +} from "./stats/index.ts"; +export { + catFromCodes, + catUnionCategories, + catIntersectCategories, + catDiffCategories, + catEqualCategories, + catSortByFreq, + catToOrdinal, + catFreqTable, + catCrossTab, + catRecode, +} from "./stats/index.ts"; +export type { + CatFromCodesOptions, + CatSortByFreqOptions, + CatCrossTabOptions, +} from "./stats/index.ts"; +export { + formatFloat, + formatPercent, + formatScientific, + formatEngineering, + formatThousands, + formatCurrency, + formatCompact, + makeFloatFormatter, + makePercentFormatter, + makeCurrencyFormatter, + applySeriesFormatter, + applyDataFrameFormatter, + seriesToString, + dataFrameToString, +} from "./stats/index.ts"; +export type { + Formatter, + SeriesToStringOptions, + DataFrameToStringOptions, +} from "./stats/index.ts"; + +// PR #120 unique modules β€” re-exported from sub-barrels +export { astypeSeries, astype, castScalar } from "./core/index.ts"; +export type { AstypeOptions, DataFrameAstypeOptions } from "./core/index.ts"; +// readExcel / xlsxSheetNames use node:zlib β€” import from "tsb/io/read_excel" directly +export { clipAdvancedSeries, clipAdvancedDataFrame } from "./stats/index.ts"; +export type { + SeriesBound, + DataFrameBound, + ClipAdvancedSeriesOptions, + ClipAdvancedDataFrameOptions, +} from "./stats/index.ts"; +export { idxminSeries, idxmaxSeries, idxminDataFrame, idxmaxDataFrame } from "./stats/index.ts"; +export type { IdxOptions, IdxDataFrameOptions } from "./stats/index.ts"; +export { modeSeries, modeDataFrame } from "./stats/index.ts"; +export type { ModeSeriesOptions, ModeDataFrameOptions } from "./stats/index.ts"; +export { + nancount, + nansum, + nanmean, + nanmedian, + nanvar, + nanstd, + nanmin, + nanmax, + nanprod, +} from "./stats/index.ts"; +export type { NanInput, NanAggOptions } from "./stats/index.ts"; +export { + nuniqueSeries, + nuniqueDataFrame, + anySeries, + allSeries, + anyDataFrame, + allDataFrame, +} from "./stats/index.ts"; +export type { + NuniqueSeriesOptions, + NuniqueDataFrameOptions, + AnyAllSeriesOptions, + AnyAllDataFrameOptions, +} from "./stats/index.ts"; +export { pctChangeSeries, pctChangeDataFrame } from "./stats/index.ts"; +export type { + PctChangeFillMethod, + PctChangeOptions, + DataFramePctChangeOptions, +} from "./stats/index.ts"; +export { quantileSeries, quantileDataFrame } from "./stats/index.ts"; +export type { + QuantileInterpolation, + QuantileSeriesOptions, + QuantileDataFrameOptions, +} from "./stats/index.ts"; +export { replaceSeries, replaceDataFrame } from "./stats/index.ts"; +export type { + ReplaceMapping, + ReplaceSpec, + ReplaceOptions, + DataFrameReplaceOptions, +} from "./stats/index.ts"; +export { varSeries, semSeries, varDataFrame, semDataFrame } from "./stats/index.ts"; +export type { VarSemSeriesOptions, VarSemDataFrameOptions } from "./stats/index.ts"; +export { skewSeries, kurtSeries, skewDataFrame, kurtDataFrame } from "./stats/index.ts"; +export type { + SkewKurtSeriesOptions, + SkewKurtDataFrameOptions, +} from "./stats/index.ts"; +export { toDatetime } from "./stats/index.ts"; +export type { DatetimeUnit, DatetimeErrors, ToDatetimeOptions } from "./stats/index.ts"; diff --git a/src/stats/index.ts b/src/stats/index.ts index b1de48eb..63864005 100644 --- a/src/stats/index.ts +++ b/src/stats/index.ts @@ -39,3 +39,324 @@ export { nsmallestDataFrame, } from "./nlargest.ts"; export type { NKeep, NTopOptions, NTopDataFrameOptions } from "./nlargest.ts"; +export { whereSeries, maskSeries, whereDataFrame, maskDataFrame } from "./where_mask.ts"; +export type { + WherePredicate, + SeriesCond, + DataFrameCond, + WhereMaskOptions, +} from "./where_mask.ts"; +export { + seriesEq, + seriesNe, + seriesLt, + seriesGt, + seriesLe, + seriesGe, + dataFrameEq, + dataFrameNe, + dataFrameLt, + dataFrameGt, + dataFrameLe, + dataFrameGe, +} from "./compare.ts"; +export type { CompareOp, SeriesOther, DataFrameOther } from "./compare.ts"; +export { shiftSeries, diffSeries, dataFrameShift, dataFrameDiff } from "./shift_diff.ts"; +export type { ShiftDiffDataFrameOptions } from "./shift_diff.ts"; +export { interpolateSeries, dataFrameInterpolate } from "./interpolate.ts"; +export type { + InterpolateMethod, + LimitDirection, + InterpolateOptions, + DataFrameInterpolateOptions, +} from "./interpolate.ts"; +export { fillnaSeries, fillnaDataFrame } from "./fillna.ts"; +export type { + FillnaMethod, + FillnaSeriesOptions, + ColumnFillMap, + FillnaDataFrameOptions, +} from "./fillna.ts"; +export { cut, qcut, cutIntervalIndex, qcutIntervalIndex } from "./cut.ts"; +export type { CutOptions, QCutOptions } from "./cut.ts"; +export { sampleSeries, sampleDataFrame } from "./sample.ts"; +export type { SampleSeriesOptions, SampleDataFrameOptions } from "./sample.ts"; +export { applySeries, applymap, dataFrameApply } from "./apply.ts"; +export type { DataFrameApplyOptions } from "./apply.ts"; + +export { + pipeSeries, + dataFramePipe, + pipeTo, + dataFramePipeTo, + pipeChain, + dataFramePipeChain, +} from "./pipe.ts"; +export { + seriesFloor, + dataFrameFloor, + seriesCeil, + dataFrameCeil, + seriesTrunc, + dataFrameTrunc, + seriesSqrt, + dataFrameSqrt, + seriesExp, + dataFrameExp, + seriesLog, + dataFrameLog, + seriesLog2, + dataFrameLog2, + seriesLog10, + dataFrameLog10, + seriesSign, + dataFrameSign, +} from "./numeric_ops.ts"; + +export { + seriesPow, + dataFramePow, + seriesMod, + dataFrameMod, + seriesFloorDiv, + dataFrameFloorDiv, +} from "./pow_mod.ts"; + +export { + seriesAdd, + seriesRadd, + seriesSub, + seriesRsub, + seriesMul, + seriesRmul, + seriesDiv, + seriesRdiv, + dataFrameAdd, + dataFrameRadd, + dataFrameSub, + dataFrameRsub, + dataFrameMul, + dataFrameRmul, + dataFrameDiv, + dataFrameRdiv, +} from "./add_sub_mul_div.ts"; + +export { getDummies, dataFrameGetDummies } from "./get_dummies.ts"; +export type { GetDummiesOptions, DataFrameGetDummiesOptions } from "./get_dummies.ts"; + +export { factorize, seriesFactorize } from "./factorize.ts"; +export type { FactorizeOptions, FactorizeResult } from "./factorize.ts"; + +export { crosstab, seriesCrosstab } from "./crosstab.ts"; +export type { AggFunc, Normalize, CrosstabOptions } from "./crosstab.ts"; + +export { toNumeric, toNumericArray, toNumericScalar, toNumericSeries } from "./to_numeric.ts"; +export type { ToNumericDowncast, ToNumericErrors, ToNumericOptions } from "./to_numeric.ts"; + +export { seriesMemoryUsage, dataFrameMemoryUsage } from "./memory_usage.ts"; +export type { MemoryUsageOptions } from "./memory_usage.ts"; + +export { selectDtypes } from "./select_dtypes.ts"; +export type { DtypeSelector, SelectDtypesOptions } from "./select_dtypes.ts"; + +export { clipSeriesWithBounds, clipDataFrameWithBounds } from "./clip_with_bounds.ts"; +export type { + BoundArg, + SeriesClipBoundsOptions, + DataFrameClipBoundsOptions, +} from "./clip_with_bounds.ts"; + +export { inferDtype } from "./infer_dtype.ts"; +export type { InferredDtype, InferDtypeOptions } from "./infer_dtype.ts"; + +export { isna, notna, isnull, notnull } from "./notna.ts"; + +export { dropna, dropnaSeries, dropnaDataFrame } from "./dropna.ts"; +export type { DropnaHow, DropnaDataFrameOptions } from "./dropna.ts"; + +export { combineFirstSeries, combineFirstDataFrame } from "./combine_first.ts"; + +export { valueCountsBinned } from "./value_counts_full.ts"; +export type { ValueCountsBinnedOptions } from "./value_counts_full.ts"; + +export { + duplicatedSeries, + duplicatedDataFrame, + dropDuplicatesSeries, + dropDuplicatesDataFrame, +} from "./duplicated.ts"; +export type { + KeepPolicy, + DuplicatedDataFrameOptions, + DuplicatedSeriesOptions, +} from "./duplicated.ts"; + +export { explodeSeries, explodeDataFrame } from "./explode.ts"; +export type { ExplodeOptions, ExplodeDataFrameOptions } from "./explode.ts"; + +export { isin, dataFrameIsin } from "./isin.ts"; +export type { IsinValues, IsinDict, DataFrameIsinValues } from "./isin.ts"; + +export { rollingSem, rollingSkew, rollingKurt, rollingQuantile } from "./window_extended.ts"; +export type { WindowExtOptions, RollingQuantileOptions } from "./window_extended.ts"; +export { fillna, countna, countValid } from "./notna_isna.ts"; +export type { IsnaInput, FillnaOptions, DropnaOptions } from "./notna_isna.ts"; +export { + strNormalize, + strGetDummies, + strExtractAll, + strRemovePrefix, + strRemoveSuffix, + strTranslate, + strCharWidth, + strByteLength, +} from "./string_ops.ts"; +export type { NormalizeForm, StrInput, ExtractAllOptions } from "./string_ops.ts"; +export { + strSplitExpand, + strExtractGroups, + strPartition, + strRPartition, + strMultiReplace, + strIndent, + strDedent, +} from "./string_ops_extended.ts"; +export type { + SplitExpandOptions, + ExtractGroupsOptions, + PartitionResult, + ReplacePair, + IndentOptions, +} from "./string_ops_extended.ts"; +export { + digitize, + histogram, + linspace, + arange, + percentileOfScore, + zscore, + minMaxNormalize, + coefficientOfVariation, + seriesDigitize, +} from "./numeric_extended.ts"; +export type { + HistogramOptions, + HistogramResult, + ZscoreOptions, + MinMaxOptions, + CvOptions, +} from "./numeric_extended.ts"; +export { + catFromCodes, + catUnionCategories, + catIntersectCategories, + catDiffCategories, + catEqualCategories, + catSortByFreq, + catToOrdinal, + catFreqTable, + catCrossTab, + catRecode, +} from "./categorical_ops.ts"; +export type { + CatFromCodesOptions, + CatSortByFreqOptions, + CatCrossTabOptions, +} from "./categorical_ops.ts"; +export { + formatFloat, + formatPercent, + formatScientific, + formatEngineering, + formatThousands, + formatCurrency, + formatCompact, + makeFloatFormatter, + makePercentFormatter, + makeCurrencyFormatter, + applySeriesFormatter, + applyDataFrameFormatter, + seriesToString, + dataFrameToString, +} from "./format_ops.ts"; +export type { + Formatter, + SeriesToStringOptions, + DataFrameToStringOptions, +} from "./format_ops.ts"; + +export { clipAdvancedSeries, clipAdvancedDataFrame } from "./clip_advanced.ts"; +export type { + SeriesBound, + DataFrameBound, + ClipAdvancedSeriesOptions, + ClipAdvancedDataFrameOptions, +} from "./clip_advanced.ts"; + +export { idxminSeries, idxmaxSeries, idxminDataFrame, idxmaxDataFrame } from "./idxmin_idxmax.ts"; +export type { IdxOptions, IdxDataFrameOptions } from "./idxmin_idxmax.ts"; + +export { modeSeries, modeDataFrame } from "./mode.ts"; +export type { ModeSeriesOptions, ModeDataFrameOptions } from "./mode.ts"; + +export { + nancount, + nansum, + nanmean, + nanmedian, + nanvar, + nanstd, + nanmin, + nanmax, + nanprod, +} from "./nancumops.ts"; +export type { NanInput, NanAggOptions } from "./nancumops.ts"; + +export { + nuniqueSeries, + nuniqueDataFrame, + anySeries, + allSeries, + anyDataFrame, + allDataFrame, +} from "./nunique.ts"; +export type { + NuniqueSeriesOptions, + NuniqueDataFrameOptions, + AnyAllSeriesOptions, + AnyAllDataFrameOptions, +} from "./nunique.ts"; + +export { pctChangeSeries, pctChangeDataFrame } from "./pct_change.ts"; +export type { + PctChangeFillMethod, + PctChangeOptions, + DataFramePctChangeOptions, +} from "./pct_change.ts"; + +export { quantileSeries, quantileDataFrame } from "./quantile.ts"; +export type { + QuantileInterpolation, + QuantileSeriesOptions, + QuantileDataFrameOptions, +} from "./quantile.ts"; + +export { replaceSeries, replaceDataFrame } from "./replace.ts"; +export type { + ReplaceMapping, + ReplaceSpec, + ReplaceOptions, + DataFrameReplaceOptions, +} from "./replace.ts"; + +export { varSeries, semSeries, varDataFrame, semDataFrame } from "./sem_var.ts"; +export type { VarSemSeriesOptions, VarSemDataFrameOptions } from "./sem_var.ts"; + +export { skewSeries, kurtSeries, skewDataFrame, kurtDataFrame } from "./skew_kurt.ts"; +export type { + SkewKurtSeriesOptions, + SkewKurtDataFrameOptions, +} from "./skew_kurt.ts"; + +export { toDatetime } from "./to_datetime.ts"; +export type { DatetimeUnit, DatetimeErrors, ToDatetimeOptions } from "./to_datetime.ts"; diff --git a/src/stats/pct_change.ts b/src/stats/pct_change.ts new file mode 100644 index 00000000..10e527ec --- /dev/null +++ b/src/stats/pct_change.ts @@ -0,0 +1,238 @@ +/** + * pct_change β€” percentage change between current and prior element. + * + * Mirrors `pandas.Series.pct_change()` / `pandas.DataFrame.pct_change()`: + * - `pctChangeSeries(series, options)` β€” per-element % change + * - `pctChangeDataFrame(df, options)` β€” column-wise % change + * + * Formula (per element i, with shift=periods): + * `result[i] = (x[i] - x[i-periods]) / x[i-periods]` + * + * When `fillMethod` is set, NaN/null values in the source are filled *before* + * computing the ratio (matching pandas' default behaviour of `fill_method="pad"`). + * + * @module + */ + +import { DataFrame } from "../core/index.ts"; +import { Series } from "../core/index.ts"; +import type { Scalar } from "../types.ts"; + +// ─── public types ───────────────────────────────────────────────────────────── + +/** Fill method applied to NaN/null before computing pct_change. */ +export type PctChangeFillMethod = "pad" | "bfill"; + +/** Options for {@link pctChangeSeries} and {@link pctChangeDataFrame}. */ +export interface PctChangeOptions { + /** + * Number of periods (lags) to shift when computing the ratio. + * Positive values look backward; negative values look forward. + * Default `1`. + */ + readonly periods?: number; + /** + * How to fill NaN/null values *before* computing the ratio. + * - `"pad"` (default): forward-fill (last valid observation carries forward). + * - `"bfill"`: backward-fill (next valid observation fills backward). + * - `null`: no filling β€” NaN/null stays as-is. + */ + readonly fillMethod?: PctChangeFillMethod | null; + /** + * Maximum number of consecutive NaN/null values to fill when `fillMethod` + * is set. `undefined` / `null` means no limit. + */ + readonly limit?: number | null; +} + +/** Options for {@link pctChangeDataFrame} β€” adds an axis selector. */ +export interface DataFramePctChangeOptions extends PctChangeOptions { + /** + * - `0` or `"index"` (default): apply operation **column-wise** (down rows). + * - `1` or `"columns"`: apply operation **row-wise** (across columns). + */ + readonly axis?: 0 | 1 | "index" | "columns"; +} + +// ─── helpers ────────────────────────────────────────────────────────────────── + +/** True when `v` is a valid number (not null, undefined, or NaN). */ +function isNum(v: Scalar): v is number { + return typeof v === "number" && !Number.isNaN(v) && v !== null; +} + +/** + * Forward-fill an array of scalars in place, respecting an optional limit. + * Returns a NEW array. + */ +function padFill(vals: readonly Scalar[], limit: number | null | undefined): Scalar[] { + const out: Scalar[] = [...vals]; + let run = 0; + let lastValid: Scalar = null; + for (let i = 0; i < out.length; i++) { + const v = out[i] as Scalar; + if (v !== null && v !== undefined && !(typeof v === "number" && Number.isNaN(v))) { + lastValid = v; + run = 0; + } else if (lastValid !== null && (limit == null || run < limit)) { + out[i] = lastValid; + run++; + } + } + return out; +} + +/** + * Backward-fill an array of scalars, respecting an optional limit. + * Returns a NEW array. + */ +function bfillFill(vals: readonly Scalar[], limit: number | null | undefined): Scalar[] { + const tmp = padFill([...vals].reverse(), limit); + return tmp.reverse(); +} + +/** Fill NaN/null in `vals` using the requested method. */ +function applyFill( + vals: readonly Scalar[], + method: PctChangeFillMethod | null | undefined, + limit: number | null | undefined, +): Scalar[] { + if (!method) { + return [...vals]; + } + return method === "pad" ? padFill(vals, limit) : bfillFill(vals, limit); +} + +/** Compute pct_change on a flat array of scalars. */ +function computePct(vals: readonly Scalar[], periods: number): Scalar[] { + const n = vals.length; + const out: Scalar[] = new Array(n).fill(null); + const shift = periods; + if (shift >= 0) { + for (let i = shift; i < n; i++) { + const curr = vals[i] as Scalar; + const prev = vals[i - shift] as Scalar; + if (isNum(curr) && isNum(prev) && prev !== 0) { + out[i] = curr / prev - 1; + } else if (isNum(curr) && isNum(prev) && prev === 0) { + // 0 denominator β†’ Infinity (same as pandas) + out[i] = + curr === 0 ? Number.NaN : curr > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; + } else { + out[i] = null; + } + } + } else { + // Negative periods: look forward + const absShift = -shift; + for (let i = 0; i < n - absShift; i++) { + const curr = vals[i] as Scalar; + const fwd = vals[i + absShift] as Scalar; + if (isNum(curr) && isNum(fwd) && curr !== 0) { + out[i] = fwd / curr - 1; + } else if (isNum(curr) && isNum(fwd) && curr === 0) { + out[i] = + fwd === 0 ? Number.NaN : fwd > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; + } else { + out[i] = null; + } + } + } + return out; +} + +// ─── public API ─────────────────────────────────────────────────────────────── + +/** + * Compute the fractional change between a Series element and the element + * `periods` positions earlier (or later, for negative `periods`). + * + * Matches `pandas.Series.pct_change()`. + * + * @example + * ```ts + * const s = new Series({ data: [100, 110, 99, 121] }); + * pctChangeSeries(s); // [null, 0.1, -0.1, 0.2222…] + * ``` + */ +export function pctChangeSeries( + series: Series, + options: PctChangeOptions = {}, +): Series { + const periods = options.periods ?? 1; + const fillMethod = options.fillMethod !== undefined ? options.fillMethod : "pad"; + const limit = options.limit ?? null; + + const filled = applyFill(series.values, fillMethod, limit); + const result = computePct(filled, periods); + + return new Series({ + data: result, + index: series.index, + name: series.name, + }); +} + +/** + * Compute percentage change for every column (or row) of a DataFrame. + * + * Matches `pandas.DataFrame.pct_change()`. + * + * @example + * ```ts + * const df = new DataFrame(new Map([ + * ["a", new Series({ data: [100, 110, 121] })], + * ["b", new Series({ data: [200, 180, 198] })], + * ])); + * pctChangeDataFrame(df); // fractional change per column + * ``` + */ +export function pctChangeDataFrame( + df: DataFrame, + options: DataFramePctChangeOptions = {}, +): DataFrame { + const axis = options.axis ?? 0; + const colWise = axis === 0 || axis === "index"; + + if (colWise) { + const colMap = new Map>(); + for (const name of df.columns.values) { + colMap.set(name, pctChangeSeries(df.col(name), options)); + } + return new DataFrame(colMap, df.index); + } + + // Row-wise: each row across columns + const periods = options.periods ?? 1; + const fillMethod = options.fillMethod !== undefined ? options.fillMethod : "pad"; + const limit = options.limit ?? null; + const nRows = df.index.size; + const cols = df.columns.values; + const nCols = cols.length; + + const resultCols = new Map(); + for (const name of cols) { + resultCols.set(name, new Array(nRows).fill(null)); + } + + for (let r = 0; r < nRows; r++) { + const row: Scalar[] = []; + for (const name of cols) { + row.push(df.col(name).values[r] as Scalar); + } + const filled = applyFill(row, fillMethod, limit); + const pct = computePct(filled, periods); + for (let c = 0; c < nCols; c++) { + (resultCols.get(cols[c] as string) as Scalar[])[r] = pct[c] as Scalar; + } + } + + const colMap = new Map>(); + for (const name of cols) { + colMap.set( + name, + new Series({ data: resultCols.get(name) as Scalar[], index: df.index, name }), + ); + } + return new DataFrame(colMap, df.index); +} diff --git a/tests/stats/pct_change.test.ts b/tests/stats/pct_change.test.ts new file mode 100644 index 00000000..f7015e4a --- /dev/null +++ b/tests/stats/pct_change.test.ts @@ -0,0 +1,252 @@ +/** + * Tests for src/stats/pct_change.ts β€” pctChangeSeries, pctChangeDataFrame + */ +import { describe, expect, it } from "bun:test"; +import fc from "fast-check"; +import { DataFrame, Series, pctChangeDataFrame, pctChangeSeries } from "../../src/index.ts"; +import type { Scalar } from "../../src/index.ts"; + +// ─── helpers ───────────────────────────────────────────────────────────────── + +function s(data: readonly Scalar[]): Series { + return new Series({ data: [...data] }); +} + +function nanEq(a: Scalar, b: Scalar): boolean { + if (typeof a === "number" && Number.isNaN(a) && typeof b === "number" && Number.isNaN(b)) { + return true; + } + return a === b; +} + +function arrEq(a: readonly Scalar[], b: readonly Scalar[]): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!nanEq(a[i] as Scalar, b[i] as Scalar)) { + return false; + } + } + return true; +} + +function close(a: Scalar, b: Scalar, eps = 1e-9): boolean { + if (a === null && b === null) { + return true; + } + if (typeof a !== "number" || typeof b !== "number") { + return false; + } + if (Number.isNaN(a) && Number.isNaN(b)) { + return true; + } + return Math.abs(a - b) < eps; +} + +function arrClose(a: readonly Scalar[], b: readonly Scalar[], eps = 1e-9): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!close(a[i] as Scalar, b[i] as Scalar, eps)) { + return false; + } + } + return true; +} + +// ─── pctChangeSeries ───────────────────────────────────────────────────────── + +describe("pctChangeSeries", () => { + it("basic increasing sequence", () => { + const result = pctChangeSeries(s([100, 110, 121, 133.1])); + expect(result.values[0]).toBeNull(); + expect(close(result.values[1] as Scalar, 0.1)).toBe(true); + expect(close(result.values[2] as Scalar, 0.1)).toBe(true); + expect(close(result.values[3] as Scalar, 0.1)).toBe(true); + }); + + it("decreasing sequence", () => { + const result = pctChangeSeries(s([200, 180, 162])); + expect(result.values[0]).toBeNull(); + expect(close(result.values[1] as Scalar, -0.1)).toBe(true); + expect(close(result.values[2] as Scalar, -0.1)).toBe(true); + }); + + it("periods=2", () => { + const result = pctChangeSeries(s([100, 105, 110, 121]), { periods: 2 }); + expect(result.values[0]).toBeNull(); + expect(result.values[1]).toBeNull(); + expect(close(result.values[2] as Scalar, 0.1)).toBe(true); + expect(close(result.values[3] as Scalar, (121 - 105) / 105)).toBe(true); + }); + + it("negative periods (look forward)", () => { + const result = pctChangeSeries(s([100, 110, 121]), { periods: -1 }); + expect(close(result.values[0] as Scalar, 0.1)).toBe(true); + expect(close(result.values[1] as Scalar, 0.1)).toBe(true); + expect(result.values[2]).toBeNull(); + }); + + it("NaN/null propagates when fillMethod=null", () => { + const result = pctChangeSeries(s([100, null, 110]), { fillMethod: null }); + expect(result.values[0]).toBeNull(); + expect(result.values[1]).toBeNull(); + expect(result.values[2]).toBeNull(); + }); + + it("fillMethod=pad fills NaN before computing", () => { + const result = pctChangeSeries(s([100, null, 110]), { fillMethod: "pad" }); + // after pad-fill: [100, 100, 110] + // pct: [null, 0, 0.1] + expect(result.values[0]).toBeNull(); + expect(close(result.values[1] as Scalar, 0)).toBe(true); + expect(close(result.values[2] as Scalar, 0.1)).toBe(true); + }); + + it("fillMethod=bfill fills NaN backward before computing", () => { + const result = pctChangeSeries(s([100, null, 110, 121]), { fillMethod: "bfill" }); + // after bfill: [100, 110, 110, 121] + // pct: [null, 0.1, 0, 0.1] + expect(result.values[0]).toBeNull(); + expect(close(result.values[1] as Scalar, 0.1)).toBe(true); + expect(close(result.values[2] as Scalar, 0)).toBe(true); + expect(close(result.values[3] as Scalar, 0.1)).toBe(true); + }); + + it("limit=1 caps forward-fill", () => { + const result = pctChangeSeries(s([100, null, null, 130]), { + fillMethod: "pad", + limit: 1, + }); + // after pad with limit=1: [100, 100, null, 130] + // pct: [null, 0, null, null] (null/100 β†’ null) + expect(result.values[0]).toBeNull(); + expect(close(result.values[1] as Scalar, 0)).toBe(true); + expect(result.values[2]).toBeNull(); + expect(result.values[3]).toBeNull(); + }); + + it("zero denominator returns Infinity", () => { + const result = pctChangeSeries(s([0, 10]), { fillMethod: null }); + expect(result.values[1]).toBe(Number.POSITIVE_INFINITY); + }); + + it("zero/zero denominator returns NaN", () => { + const result = pctChangeSeries(s([0, 0]), { fillMethod: null }); + expect(Number.isNaN(result.values[1] as number)).toBe(true); + }); + + it("preserves Series name and index", () => { + const src = new Series({ data: [10, 20, 30], name: "price" }); + const result = pctChangeSeries(src); + expect(result.name).toBe("price"); + expect(result.index.size).toBe(3); + }); + + it("empty series returns empty", () => { + const result = pctChangeSeries(s([])); + expect(result.values.length).toBe(0); + }); + + it("single-element series returns [null]", () => { + const result = pctChangeSeries(s([42])); + expect(result.values[0]).toBeNull(); + }); +}); + +// ─── pctChangeDataFrame ─────────────────────────────────────────────────────── + +describe("pctChangeDataFrame", () => { + it("column-wise (default)", () => { + const df = DataFrame.fromColumns({ + a: [100, 110, 121], + b: [200, 180, 198], + }); + const result = pctChangeDataFrame(df); + const colA = result.col("a").values; + const colB = result.col("b").values; + expect(colA[0]).toBeNull(); + expect(close(colA[1] as Scalar, 0.1)).toBe(true); + expect(close(colA[2] as Scalar, 0.1)).toBe(true); + expect(colB[0]).toBeNull(); + expect(close(colB[1] as Scalar, -0.1)).toBe(true); + expect(close(colB[2] as Scalar, 0.1)).toBe(true); + }); + + it("row-wise (axis=1)", () => { + const df = DataFrame.fromColumns({ + a: [100, 200], + b: [110, 220], + c: [121, 242], + }); + const result = pctChangeDataFrame(df, { axis: 1 }); + // row 0: [100, 110, 121] β†’ [null, 0.1, 0.1] + // row 1: [200, 220, 242] β†’ [null, 0.1, 0.1] + const row0a = result.col("a").values[0]; + const row0b = result.col("b").values[0]; + const row0c = result.col("c").values[0]; + expect(row0a).toBeNull(); + expect(close(row0b as Scalar, 0.1)).toBe(true); + expect(close(row0c as Scalar, 0.1)).toBe(true); + const row1a = result.col("a").values[1]; + const row1b = result.col("b").values[1]; + expect(row1a).toBeNull(); + expect(close(row1b as Scalar, 0.1)).toBe(true); + }); + + it("preserves column order", () => { + const df = DataFrame.fromColumns({ + x: [1, 2], + y: [3, 6], + }); + const result = pctChangeDataFrame(df); + expect(result.columns.values).toEqual(["x", "y"]); + }); +}); + +// ─── property-based tests ───────────────────────────────────────────────────── + +describe("pctChangeSeries β€” property tests", () => { + it("result length equals input length", () => { + fc.assert( + fc.property(fc.array(fc.float({ noNaN: true }), { minLength: 0, maxLength: 50 }), (arr) => { + const result = pctChangeSeries(s(arr)); + return result.values.length === arr.length; + }), + ); + }); + + it("first element is always null for periods=1", () => { + fc.assert( + fc.property(fc.array(fc.float({ noNaN: true }), { minLength: 1, maxLength: 50 }), (arr) => { + const result = pctChangeSeries(s(arr)); + return result.values[0] === null; + }), + ); + }); + + it("pct_change(x, -p) equals pct_change_reversed pattern", () => { + // For a sequence of positive numbers with periods=1 and periods=-1: + // result[-1][i] represents the change looking forward, so result[-1][i] = (x[i+1]-x[i])/x[i] + // and result[+1][i+1] = (x[i+1]-x[i])/x[i], so they should agree on matching indices + fc.assert( + fc.property( + fc.array(fc.float({ noNaN: true, min: 1, max: 1000 }), { minLength: 3, maxLength: 20 }), + (arr) => { + const fwd = pctChangeSeries(s(arr), { periods: -1, fillMethod: null }); + const bwd = pctChangeSeries(s(arr), { periods: 1, fillMethod: null }); + // fwd[i] = (arr[i+1] - arr[i]) / arr[i] + // bwd[i+1] = (arr[i+1] - arr[i]) / arr[i] ← same ratio + for (let i = 0; i < arr.length - 1; i++) { + if (!close(fwd.values[i] as Scalar, bwd.values[i + 1] as Scalar, 1e-6)) { + return false; + } + } + return true; + }, + ), + ); + }); +});