Changelog
Source:NEWS.md
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.
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
First CRAN release. No scaled cycle-time values change and no exported function changes behavior – the major version marks the move to 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.
Switching anchor changes no row’s inclusion.
cyclic_timeandcyclic_time_ovscale the same rows as each other, and so docyclic_time_imputeandcyclic_time_imp_ov. This held oncycledataand on a copy chopped to create a left-censored opening tail and an open trailing cycle. Switching between confirmed-only and imputation-inclusive does change coverage: 358 of 744 rows against 735 incycledata, and 0 against 744 in a copy withovtodayzeroed throughout.Each variable places one anchor at zero and the other at the wrap. On
cyclic_timemenses onset is at zero and ovulation is at-1/+1; oncyclic_time_ovthe reverse. A feature at the wrap is split across the two ends of the plotted axis, so an arithmetic mean of its position is meaningless and the summary has to be circular.
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:
- Omitted is an error, with a message naming both ways forward. Someone who has ovulation data and simply forgot the argument must not silently receive an all-imputed analysis.
-
NULLtakes the imputed path described above. -
A column name that is not in
datastill errors and lists the available columns, so a typo cannot be mistaken for “no biomarker”.
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
launch_app()did not check forwritexl. The Shiny app callswritexl::write_xlsx()for its download buttons and loads the package at startup, butwritexlwas declared nowhere and the dependency gate checked onlyshinyjsandcpass. A user holding both gated packages got a clean pass fromlaunch_app()and then hit an error before the app rendered.writexlis now a suggested dependency and is checked alongside the other two.launch_app()now documents its return value, as CRAN requires of exported functions.The vignettes required R 4.1 while
DESCRIPTIONdeclared 3.5. Nine uses of the native|>pipe have been replaced with%>%. The package’s own code was already free of 4.1-only syntax, so only vignette building was affected, and only on R 4.0 or older.tidyverseis no longer a suggested dependency. The overview vignette loaded the whole suite but used onlydplyrandggplot2, both already inImports. It now loads those two, and mentionstidyverseas an alternative for readers who have it.cycle_plot()usedpartial = Trather thanTRUE.
Documentation corrections
An audit of all package documentation against Nagpal et al. (2025) produced the following.
Ovulation-biomarker precision.
?summary_ovulationand?pacts_scalingsaid biomarker confirmation gives “more precise” or “greater precision” estimates. Section 2.1.2 of the paper says a positive LH-surge test and a BBT nadir do not pinpoint the day of ovulation, which would need ultrasound; they place it within 24 to 36 hours, and the window depends on the method. Both files now say that instead.Statistical significance is not effect size. The overview vignette read two significant random-effects p-values as indicating “meaningful” variation and heterogeneity. They indicate detectable variation at this sample size. Corrected in both places, since that section exists to teach GAMM output interpretation.
What a smoothing penalty does. The vignette said the wiggliness penalty ensures the model “captures important trends without overfitting noise.” It now says the penalty trades bias for variance and can oversmooth a real feature at small n.
Quantified two vague claims using the paper’s own figures: the day -15 backward count differs from hormone-confirmed ovulation by a mean absolute 0.97 days (SD 0.88) across 33 cycles, with error growing with cycle length (r = 0.395).
“biomarker” is now “ovulation biomarker” in the 18 places it stood alone as a noun, matching the paper’s own usage in Sections 1.3 and 2.1.1. Compound forms such as “biomarker-confirmed ovulation” are unchanged, since the noun already names what is measured. One section heading changed with its cross-reference.
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
Confirmed ovulation with no later menses in the data. Situation: a participant’s last row is N days after a confirmed ovulation, and no menses onset is observed after that ovulation. Before:
cyclic_time,cyclic_time_ov, andluteal_lengthtreated day N as the last day of the luteal phase. Example: with N = 6, day 6 received the value of the day before menses and day 3 received the value of mid-luteal. Now: those N days areNAin all three columns. The same days can be scaled withimpute_next_menses = TRUE, which adds an onset at ovulation + 14 days (LH + 15), setsmenses_impute = 1on that row, and scales the days in the imputed columns. This behavior existed in every release before 0.1.7.POSIXctdates. Situation: the date column isPOSIXctand a value has a non-midnight time (for example 2024-03-01 09:00). Before: that value becameNAand the row was dropped; if the dropped row was an ovulation day, the ovulation was lost; if every row had a non-midnight time, the function stopped with an internal error. Now: the time is dropped and the date is kept. A value that cannot be read as a date produces a warning.impute_next_menseswith a late closing menses. Situation:impute_next_menses = TRUE, a confirmed ovulation, and the next observed menses onset more than 20 days after that ovulation. Before: the observed onset was not found, and an imputed onset was added at ovulation + 14 days even though an observed onset existed. Example: an observed 22-day luteal phase received an imputed onset on day 14. Now: the search for an observed onset extends to the participant’s next confirmed ovulation, including an onset on that ovulation’s date, and no onset is imputed when one is found.next_menses_max_windowno longer has an effect and warns if supplied.Luteal phase over 18 days or follicular phase over 25 days, with confirmed ovulation. Situation: a confirmed-ovulation cycle within
lower_cyclength_boundandupper_cyclength_boundin which the luteal phase exceeds 18 days or the follicular phase exceeds 25 days. Before: the days beyond the cap wereNAincyclic_time_imputeandcyclic_time_imp_ov, as well as incyclic_timeandcyclic_time_ov. Now:cyclic_time_imputeandcyclic_time_imp_ovare computed for those days using the observed phase length, provided each phase is at least its minimum (7 days luteal, 8 days follicular). New columnscyclic_time_impute_extended_phaseandcyclic_time_imp_ov_extended_phaseequal 1 on the days filled this way and 0 otherwise.cyclic_timeandcyclic_time_ovare unchanged.
New arguments to pacts_scaling()
luteal_phase_min_days(7),luteal_phase_max_days(18),follicular_phase_min_days(8),follicular_phase_max_days(25). These were fixed values before 0.1.7. Defaults give the same output as 0.1.6. Raising a maximum extendscyclic_timeandcyclic_time_ovto the longer phase. Lowering a maximum does not changecyclic_time_imputeorcyclic_time_imp_ov, because the fallback in the previous bullet does not use the maximum. Details in?pacts_scaling, section “Internal phase-length caps”.Input validation. Before: a missing column stopped with an rlang error;
mensesorovtodaycoded as text ("yes"/"no") or as bleeding intensity (0, 1, 2, 3) returned every derived column asNAwith no message. Now: each case stops with an error that names the column and the problem.
Documentation and packaging
-
?pacts_scalingdocuments the four new arguments, the phase-length caps, and every returned column includingcyclic_time,cyclic_time_impute,cyclic_time_ov, andcyclic_time_imp_ov.?menstrualcycleRadded. The vignette’s description ofimpute_next_mensesupdated. -
R CMD check: 0 errors, 0 warnings, 0 notes. Debugprint()output removed frompacts_scaling().mgcv,shinyjs, andcpassmoved from Imports to Suggests. Regression tests added for each fix.
menstrualcycleR 0.1.6
- New opt-in argument
impute_next_mensesinpacts_scaling()(defaultFALSE). WhenTRUE, a next-menses onset is imputed forward from a biomarker-confirmed ovulation that has no recorded closing menses, placed atovulation + next_menses_luteal_days(default 14 days, the population-average luteal length, i.e. the “LH+15” / last-follicular-day convention). That cycle then becomes scalable instead of being dropped for a missing anchor. Imputed onsets are flagged in a newmenses_imputecolumn so imputed and observed onsets stay distinguishable. - Two companion arguments tune the rule:
next_menses_luteal_days(default 14) andnext_menses_max_window(default 20; skip imputation for an ovulation if an observed menses already closes the cycle within that many days). - This complements the existing ovulation imputation, which imputes ovulation backward from an observed menses (menses minus 15). The package can now close a cycle from either direction when one anchor is missing.
- The default (
FALSE) leaves all previous behavior byte for byte identical; only callers who opt in see any change. Only the general rule is applied. Study or protocol specific gating (for example blocking imputation across treatment phases or documented off-study breaks) remains the caller’s responsibility.
menstrualcycleR 0.1.5
- Maintainer changed to Tory Eisenlohr-Moul.
- Documentation fixes: the
cycledatahelp page now documents thedateratedcolumn (it previously saiddate, which the dataset does not contain), andcycle_plot()’salign_valargument is now documented under its correct name (wasalignval). - Build/packaging hygiene: the pkgdown site (
docs/) and shinyapps deployment records (rsconnect/,inst/shiny/rsconnect/) are no longer bundled into the package tarball; removed a strayR/.Rapp.history; tidied a vignette chunk label that produced a non-portable figure filename. No effect on installed functionality. - Internal: namespace-qualified
stats::sd()/stats::ave(), importedrlang’s:=, and registered remaining non-standard-evaluation column names, clearing the “no visible binding for global variable” check notes. No user-facing change.
menstrualcycleR 0.1.4
- Documentation/metadata only — no code changes. Removed a dead OSF preprint link from the
URL:field ofDESCRIPTIONand added a “How to cite” section to the README pointing to the published paper (Nagpal et al., 2025, Psychoneuroendocrinology). The canonical citation remains available viacitation("menstrualcycleR")(seeinst/CITATION).
menstrualcycleR 0.1.3
Fixed a bug in
pacts_scaling()where the user’s original date column was leftNAon fabricated calendar rows. To compute cycle lengths,pacts_scaling()internally densifies each participant’s calendar so that every missing day between their first and last observation becomes a row. Those fabricated rows received the package’s internal canonicaldate, but the column you passed in (e.g.daterated) was leftNAon them. When a cycle’s ovulation had to be imputed (no biomarker-confirmed ovulation), the imputed-ovulation day — along with itscyclic_time*/scaled_cycleday*values — could land on exactly such a fabricated row. A downstream analysis that joined the PACTS output back to other data by the original date column name would then silently drop those rows, quietly losing imputed-ovulation and scaled cycle-time observations.pacts_scaling()now returns a fully populated original date column: on fabricated rows it is filled from the internal calendar date, and observed dates are never overwritten. Character- and factor-typed date columns (a supported input) are coerced toDatethe same way the rest of the pipeline already coerces them;DateandPOSIXctdate columns keep their type. A regression test asserts that no row carries a realdateor a scaled/cycle value while the original date column isNA, acrossDate, character, factor, andPOSIXctdate inputs.Fixed the same class of bug for the user’s original ID column. The internal densify keeps the package’s canonical
idpopulated on fabricated calendar rows (it is the grouping key), but an original id column passed under a different name (e.g.record_id,subject) was leftNAon those rows — so a downstream join on id + date could still drop imputed-ovulation / scaled rows even after the date fix.pacts_scaling()now also refills the original id column from the canonicalidon fabricated rows (NA-fill only; observed ids are never changed and the column’s type is preserved). This was latent in typical use because the package’s own examples name the id columnid; it surfaces for datasets that use any other id column name. A regression test covers character and integer id columns under a non-idname.Fixed a bug in
pacts_scaling()where the last participant in a dataset could receiveNAfor the scaled cycle-time variables across an entire phase (most visibly the luteal phase, i.e.scaled_cycledayafterovtoday == 1). The internal phase-length loops (lutmax,folmax,folmax_imputeinhelper.R) detected the end of a phase by looking one row ahead and stopped atnrow(data) - 1, so the final row of the dataset was never treated as the end of a run. The result was position-dependent: reordering participants so that another came last moved the problem to that participant. The loops now treat the final dataset row as a valid run-end, and a regression test asserts that a participant’s scaled output is identical regardless of their position in the dataset. Thanks to Elisabeth Conrad (Freie Universität Berlin) for the clear bug report and reproduction. ([reported via PACTS user correspondence, June 2026])Packaging hygiene: declared
purrrinImports(used inhelper.Rbut previously undeclared), and addedtidyverseandmarginaleffectstoSuggestsso the overview vignette builds in a clean environment. Continuous integration (GitHub ActionsR-CMD-check) now runs the fullR CMD check— including the vignette — on every push and pull request.pacts_scaling()andcycle_plot_individual()no longer requiredplyr/tidyverseto be attached. Several internal calls to dplyr verbs (ungroup(),filter(),case_when(),first()) and torlang::sym()were not namespace-qualified, so a barelibrary(menstrualcycleR)producedError: could not find function "ungroup". All such calls are now qualified (dplyr::/rlang::), and every exported function works with the package loaded on its own.
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
-
pacts_scaling()computes continuous, phase-aligned cycle-time variables — the recommendedcyclic_time*measures (which map -1 and +1 to the same hormonal point) plus thescaled_cycleday*measures — centered on menses onset or ovulation, with optional ovulation imputation. -
cycle_plot()andcycle_plot_individual()visualize outcomes across the standardized cycle at the group and individual level, with rolling-average smoothing. -
cycledata_check()andsummary_ovulation()summarize data availability and confirmed-versus-imputed ovulation. -
launch_app()opens an interactive Shiny app for cycle scaling, data checks, visualization, and C-PASS (PMDD/MRMD/PME) diagnosis. -
cycledataprovides an example daily-diary dataset. - Vignette Getting Started with menstrualcycleR and Phase-Aligned Cycle Time Scaling (PACTS) walks through the full workflow, including GAMM modeling with
mgcvcyclic cubic regression splines.