menstrualcycleR 1.1.1

CRAN resubmission. Three changes, all requested by the CRAN reviewer on 2026-10-10; no scaled cycle-time value changes and no exported function changes behaviour.

The title no longer opens with “Tools for”. Title is now “Phase-Aligned Cycle Time Scaling (PACTS) and Menstrual Cycle Analysis”. CRAN treats a leading “Tools for” as redundant, since every package is a set of tools.

T and F are written out as TRUE and FALSE. cycle_plot()’s se argument defaulted to F, which is what the reviewer saw in the help page, and cycle_plot_individual() passed partial = T to zoo::rollapply() in three places. T and F are ordinary variable names in R, not reserved words, so code that relies on them breaks for anyone who happens to have assigned them – which is why CRAN asks for the spelled-out forms. The “Getting Started” vignette’s se = T calls and its chunk options are spelled out too, since they are the code a reader copies.

No other change. The default of se is still FALSE and every result is identical.

menstrualcycleR 1.1.0

New practice dataset: cycledata_special

A second practice dataset ships alongside cycledata, built for one job: showing what each of pacts_scaling()’s settings actually does. It holds 15 people and 998 rows, and every person is one special case, named in words in a new case column.

The dataset exists because cycledata cannot do this. All 25 of its people have every cycle inside 21-35 days, not one was observed before their first recorded menses onset, none has a confirmed ovulation with no closing onset, and none has a calendar gap – so the settings added in this version and in 0.1.7 change nothing at all when run on it. A dataset in which nothing unusual happens can neither demonstrate a setting working nor catch a claim about one that is wrong. That blind spot is what let the ovulation-imputation behaviour of the cycle-length bounds go undocumented for as long as it did.

The fifteen: an ordinary three-cycle record, to compare against; 8 and then 30 rated days before a first recorded onset (the first fabricates 7 rows, the second shows how far the setting reaches); a confirmed ovulation among the leading days, which scales with no setting at all and makes impute_leading_ovulation correctly decline; a confirmed ovulation at the end of the diary with no closing onset; an 18-day and a 42-day cycle with ovulation unconfirmed, on the wrong side of lower_cyclength_bound and upper_cyclength_bound; a luteal phase past luteal_phase_max_days and a follicular phase past follicular_phase_max_days, both recovered by the phase-cap fallback and flagged; a luteal phase under luteal_phase_min_days, recovered by nothing, which is the asymmetry between the phase floors and the phase ceilings; an eleven-day hole in a diary, to show the filled rows coming back with no rating on them; a person with ovulation confirmed on no day; a person with no anchor of any kind, whose columns are empty for a reason; and a person with one confirmed ovulation and no onset ever recorded, whose single day scales pinned at cyclic_time = 1 and whose whole luteal phase scales once impute_next_menses closes the cycle; and a person with leading days AND an unclosed trailing ovulation, who is the only one both opt-in rules act on at once.

Every one of those claims is checked by an automatic test rather than only written down (tests/testthat/test_cycledata_special.R, 94 checks), so a change to a default that stops a person demonstrating their setting fails a test instead of leaving a false sentence in the help page. The dataset is generated, not real and not derived from any real record, and the script that builds it – asserting each case’s arithmetic as it runs – ships in the package sources at data-raw/cycledata_special.R.

Both vignettes now point at it. “Preparing Your Data” gains a section, “When your data is awkward”, that works through four of the cases with live output: how far apart a setting’s recovered ROWS and recovered DATA are (45 days gained, 31 of them carrying a rating, 12 rows fabricated), that leading_ovulation_luteal_days scales linearly with whatever it is passed, which person no combination of settings can reach and why, and what the NAs in the case column are for. The overview vignette introduces the dataset where it introduces cycledata.

Two things building it established about existing behaviour

Neither is a code change; both are corrections to what the documentation said.

leading_ovulation_luteal_days has no upper bound of any kind. The 15 is where the ovulation is placed by default, not a cap on how far back the rule reaches, and the documentation added with the setting in this version implied otherwise. Setting it to 25 scales 25 leading days; setting it to 40 scales 40 and fabricates rows before the diary begins. The cycle-length bounds cannot gate it (a left-censored tail has no cycle length to test, which is the reason the default declines to scale it at all) and the phase-length caps do not: a 25-day imputed luteal phase scales with luteal_phase_max_days left at 18. A value above the default is a methods decision to record, not a recovery dial. The help page for the new dataset now says so, and person 3 demonstrates it.

The two opt-in rules are applied in a fixed order, and it does not matter. impute_next_menses runs first and impute_leading_ovulation second, so the leading rule can see an imputed onset. On a person who has both – person 15 of the new dataset – the days gained together are exactly the sum of the days each gains alone. And an imputed onset can never become a leading-day anchor, structurally: impute_next_menses only imputes an onset forward from a confirmed ovulation, so whenever that onset is a person’s first, a confirmed ovulation necessarily precedes it, which is the condition on which the leading rule declines.

Smaller fixes

cycle_plot() and cycle_plot_individual() passed size to their line layer, which ggplot2 deprecated in 3.4.0. Every call therefore printed a deprecation warning telling the user to report an issue against this package. Both now pass linewidth, and DESCRIPTION records the ggplot2 (>= 3.4.0) floor that linewidth needs. These were the only two size arguments in the plotting code, so nothing that legitimately still takes size (point and text sizes) is touched.

cycle_plot_individual() stopped with “No data found for ID x” in two quite different situations: when no row had that id at all, and when the participant had plenty of rows but none belonging to a numbered menses-to-menses cycle. The second is not missing data – it is a participant with no recorded menses onset, or none that closes a cycle – and the old message sent a reader looking for a data problem that was not there. The two cases now have separate messages, and the second names ?cycledata_special, which ships worked examples of both.

The comment in .github/workflows/R-CMD-check.yaml claimed the vignettes’ build-time dependencies including tidyverse were declared in Suggests. tidyverse was dropped from Suggests for CRAN and is not needed: the vignette loads it only inside an eval = FALSE chunk that shows a reader what to install. Comment corrected; no configuration change.

Packaging fixes

pacts_scaling()’s help page had not been regenerated after this version’s new settings were added, so the help page and the function disagreed about which settings exist. R CMD check --as-cran reports that as a WARNING, and a warning is a rejection.

ovtoday_leading_impute was missing from the list of column names the package declares it expects, which R flags when checking the code.

Two files the testing tool writes when a check fails (tests/testthat/testthat-problems.rds and tests/testthat/_problems/) had been committed and would have shipped inside the package. Removed, and excluded from future builds.

Bug fix: first follicular phase lost at a participant boundary

The follicular-phase pass closed a participant’s run on their FIRST row whenever the row before it – the previous participant’s last row – was an ovulation day; the check looked at row i-1 without confirming it belonged to the same participant. That participant’s whole first follicular phase was then never scaled (in every column). Present in every prior release; on a 125-participant study dataset it affected 3 participants and 43 person-days. Fixed by requiring the previous row to belong to the same participant. Results for everyone else are unchanged (tested against the existing suite).

New opt-in rule: impute_leading_ovulation

pacts_scaling() gains impute_leading_ovulation = FALSE (with leading_ovulation_luteal_days = 15). When TRUE, days a participant was observed BEFORE their first recorded menses onset – the left-censored start of participation, which previously could never be scaled because the luteal phase they belong to had a closing menses but no ovulation – get an ovulation imputed at the first onset minus 15 days, the same backward count the package uses inside observed cycles. The leading days then scale as the end of a luteal phase in cyclic_time_impute / cyclic_time_imp_ov only; the confirmed-only columns are untouched. The imputed anchor is marked in a new column ovtoday_leading_impute, and a blank row is added when the imputed day precedes the first observed row (as impute_next_menses does for an imputed onset). Nothing is imputed when a confirmed ovulation already lies before the first onset. The default FALSE keeps every existing result byte-for-byte identical (tested). Requested by the CLEAR Lab ADHD-Cycle analysis (2026-09-27), where roughly a third of participants began the diary in a luteal phase.

menstrualcycleR 1.0.0

Prepared for CRAN, but never submitted: the packaging work below cleared the blockers, and 1.1.0 superseded this version before a submission was made, so CRAN has never carried 1.0.0. (Recorded here because this entry read “First CRAN release” until 2026-09-29, which was never true.) No scaled cycle-time values change and no exported function changes behavior – the major version marks readiness for CRAN, not a break with 0.1.9. Code written against 0.1.9 runs unchanged.

Packaging for CRAN

cpass is no longer listed in Suggests, and the Remotes: lasy/cpass field is gone. CRAN does not accept a Remotes field, and does not accept a suggested package that is not in a mainstream repository. launch_app() is unchanged: it still checks for both shinyjs and cpass with requireNamespace() and reports whichever is missing, so the app’s CPASS tab behaves exactly as before for anyone who installed cpass from GitHub. requireNamespace() does not require a package to be declared, so dropping the declaration costs nothing at run time. Only the documentation wording changed, to say that shinyjs comes from CRAN and cpass from GitHub.

The Description field is rewritten. It no longer opens with the package name, which CRAN policy disallows, and it now cites the PACTS paper by DOI.

Three documentation links pointed at the retired eisenlohrmoullab.github.io domain and now point at menstrualcycler.clearlabresearch.com directly rather than relying on the redirect. Documentation is regenerated with roxygen2 8.1.0, which replaces the RoxygenNote field with Config/roxygen2/version.

New vignette: Preparing Your Data for PACTS

vignette("preparing-your-data") covers getting data into the shape pacts_scaling() expects, and choosing among the four cycle-time variables it returns. It starts from the case the overview vignette assumes away: a period-tracker export listing period start dates, a separate table of daily measurements, and no ovulation biomarker anywhere. It gives the join that turns the first into a daily menses flag, notes on reshaping Oura, Apple Health and Fitbit exports, a pre-scaling check list, and the reporting that belongs in a methods section.

Two points in it are not stated elsewhere in the documentation.

The vignette states what the published paper does not: no confirmation rate has been established below which ovulation-anchored questions become unanswerable. In its place it gives the sensitivity analysis Nagpal et al. (2025) ran on their own 44 cycles – fit on confirmed cycles only and on the full set, and report both.

ovtoday = NULL for studies with no ovulation biomarker

pacts_scaling() previously required an ovtoday column even when none had been measured, and the documentation told such users to fabricate a column of zeros. Read as a hard requirement, that turned the most common wearable-study situation into a wall.

ovtoday now defaults to NULL, meaning “no ovulation biomarker was collected”. The column is created as all-NA, every cycle takes the imputed-ovulation path, and a message says so and points at summary_ovulation(). Results are identical to supplying the column by hand.

Three states are kept deliberately distinct, and the distinction is the point:

Passing NULL when an ovtoday column exists warns, and honors the NULL.

Previously, omitting the argument produced argument "x" is missing, with no default, which named an internal helper’s parameter rather than ovtoday.

Covered by tests/testthat/test_ovtoday_null_path.R, which exists mainly to hold the three states apart. The first implementation collapsed omitted into NULL, because giving the argument a default makes rlang::quo_is_missing() permanently FALSE; only base::missing(), read before enquo() rebinds the name, separates them.

Fixes found in a pre-submission review

Documentation corrections

An audit of all package documentation against Nagpal et al. (2025) produced the following.

Verified unchanged: every pasted statistic in the overview vignette still matches a live knit (n = 611, R-sq.(adj) = 0.528, deviance explained 55.5%, and both p-values).

New section: Outstanding Questions

The overview vignette gains a section naming four unresolved areas, so that defaults are not read as validated thresholds: statistical power for PACTS designs, cyclical clustering and subgroup identification, the reliability of timing features read off per-person smooths, and how much cycle coverage a person needs before their data support an estimate. cycledata_check() reports coverage and deliberately sets no threshold.

summary_ovulation() reports cycles outside 21-35 days

ovstatus_id gains a column, Total cycles with cycle length < 21 or > 35. The per-cycle flag behind it has existed since 0.1.9 but the roll-up line was commented out, so the value was computed and discarded.

Enabling it needed one fix. The flag is NA on a still-open trailing cycle, where mcyclength_complete is NA and the cycle’s length is not yet knowable, and the roll-up used a bare sum(). Every participant in cycledata has such a cycle, so the column would have come back NA for all 25 of them. The roll-up now passes na.rm = TRUE.

The column is descriptive, not an inclusion criterion. A confirmed-ovulation cycle outside 21-35 days is still scaled, gated by its phase lengths. It is reported so that this divergence from Nagpal et al. (2025) Section 2.1.1 is visible in output rather than only in prose – see the lower_cyclength_bound documentation in ?pacts_scaling, which now states the divergence explicitly, as do both vignettes and the README quick start at the point where pacts_scaling() is called.

Use of AI coding tools

Claude Code was used for this release: the CRAN packaging changes above, the new vignette, and this entry. The decision to use it was made by Dr. Tory Eisenlohr-Moul, the package maintainer, who reviewed and approved every change.

No AI coding tool was used in the package’s initial development, which began in January 2025. Anisha Nagpal’s contributions predate all AI tool use: her final commit is dated 18 March 2026, and the first AI-assisted commit is dated 30 May 2026. Of 567 commits at the time of this release, 49 carry a Claude Code Co-Authored-By trailer (from June 2026) and 2 are from GitHub Copilot’s coding agent (May 2026, updating inst/CITATION and the startup citation in R/zzzz.R).

The README carries this statement permanently. No AI system is listed in Authors@R.

menstrualcycleR 0.1.9

Adds mcyclength_complete, returned alongside mcyclength from pacts_scaling(). Situation: on cycle_incomplete == 1 rows, mcyclength is days-observed-so-far in a still-open trailing cycle, not a cycle length. Filtering on mcyclength alone, without a separate cycle_incomplete == 0 clause, silently admits those rows whenever the observed-so-far count happens to land in range. Before: getting this right required remembering that second clause every time (documented since 0.1.8, but easy to miss – a downstream analysis filtered on mcyclength alone and admitted 263 person-days from a still-open cycle before the omission was caught). Now: mcyclength_complete is NA whenever cycle_incomplete == 1, so filter(mcyclength_complete >= 21, mcyclength_complete <= 35) excludes those rows automatically, with no second clause needed. mcyclength itself is unchanged.

Also fixes the same defect internally in summary_ovulation()’s cycles_outside_norm calculation, which compared mcyclength (not mcyclength_complete) to its [21,35] reference norm. This value is currently dropped before summary_ovulation() returns, so no user-visible output changes – the fix is so the computation is correct if this column is ever exposed later, rather than a landmine waiting to be uncovered.

One clarification while documenting this, since it is easy to over-read the 0.1.8 correction as “the length bound never matters”: lower_cyclength_bound/ upper_cyclength_bound fully gate cycle-time computation for a cycle with no confirmed ovulation – a closed cycle outside the bound gets zero cyclic_time_impute coverage, not partial, until the bound is widened to include it. The bound does not gate a confirmed-ovulation cycle, which scales on its phase lengths regardless of mcyclength (see the vignette’s “Cycle Length Inclusion Criteria” section). The published PACTS paper’s summary of this rule (Nagpal et al., 2025, Psychoneuroendocrinology 181:107584, Section 2.1.1) predates the 0.1.8 correction and does not distinguish the two cases.

Also corrects several pre-existing documentation errors found while reviewing the above, none related to mcyclength_complete: the vignette’s “Interpreting the GAMM Summary Output” section had a pasted model-fit snapshot (n=575, R-sq.(adj)=0.533) that had drifted since the 0.1.7 phase-cap fallback and no longer matched a live run of the vignette’s own code (n=611, R-sq.(adj)=0.528) – every number in that section is now current. A “574” in the data-availability section contradicted “575” from the live chunk directly above it. Three citations (?pacts_scaling, ?summary_ovulation, the vignette’s reference list) pointed at the retired OSF preprint instead of the published paper. process_luteal_phase_base() was missing two @param entries. A couple of smaller copy-paste/staleness fixes round it out – see the git history for the full list.

menstrualcycleR 0.1.8

Corrects some errors in the vignette and fixes a few small bugs. No scaled cycle-time values change for any existing user.

To clarify how scaling eligibility works: lower_cyclength_bound / upper_cyclength_bound determine which cycles are eligible for an imputed ovulation. A cycle with a confirmed ovulation is scaled based on its phase lengths (luteal 7–18 days, follicular 8–25 days, each adjustable). The vignette now describes this accurately, and shows how to filter by cycle length after scaling for users who want that.

Also fixes a crash affecting complete cycles of 14 days or fewer when eligible for ovulation imputation, plus a few minor internal corrections.

menstrualcycleR 0.1.7

Four bug fixes that change scaled cycle-time values in specific situations, four new arguments, input validation, and documentation updates. Each fix states the situation it applies to.

Bug fixes

New arguments to pacts_scaling()

Documentation and packaging

menstrualcycleR 0.1.6

menstrualcycleR 0.1.5

menstrualcycleR 0.1.4

menstrualcycleR 0.1.3

menstrualcycleR 0.1.0

First release of menstrualcycleR, the companion R package to:

Nagpal, A., Schmalenberger, K. M., Barone, J. C., Mulligan, E., Stumper, A., Knol, L., Failenschmid, J., Kiesner, J., Peters, J. R., & Eisenlohr-Moul, T. A. (2025). Studying the Menstrual Cycle as a Continuous Variable: Implementing Phase-Aligned Cycle Time Scaling (PACTS) with the menstrualcycleR package. Psychoneuroendocrinology, 107584. https://doi.org/10.1016/j.psyneuen.2025.107584

Core functionality

mirror server hosted at Truenetwork, Russian Federation.