Practice dataset of special cases: one person per setting
Source:R/cycledata_special.R
cycledata_special.RdA second, deliberately awkward practice dataset. Where cycledata is an
ordinary daily diary, every person here exists to exercise exactly one
situation that pacts_scaling() has a setting for, or one situation no
setting can rescue. The case column names which. Use it to see what a
setting actually changes before switching it on for your own data, and to
recognise a person whose columns come back empty for a reason.
Format
A data frame with 998 rows and 6 variables:
- id
Participant ID, 1 to 15. One person per case.
- daterated
Date of observation
- symptom
Daily rating, 0 to 10, with a perimenstrual peak.
NAon roughly 8 percent of days, at random, so that coverage figures have to be counted over days that were actually rated.- menses
1on the first day of menses,0otherwise- ovtoday
1on a biomarker-confirmed day of ovulation,0otherwise- case
Which special case this person is, in words. Reads
NAon rows the package itself adds – calendar-filled gaps and imputed anchors – which is deliberate: it is the cheapest way to see which rows in your output nobody rated. Carry it across those rows withdplyr::group_by(id) |> tidyr::fill(case, .direction = "downup").
Source
Generated, not real and not derived from any real record: the
anchors are written out by hand and the symptom values come from a fixed
random seed. The script that builds it, with the arithmetic for every case
asserted as it runs, is data-raw/cycledata_special.R in the package
sources; tests/testthat/test_cycledata_special.R checks that each case
still behaves as its label says, so a change to a default that stops a
person demonstrating their setting fails a test instead of passing
silently.
Details
cycledata cannot do this job: all 25 of its people have every cycle in
range, none was observed before their first recorded menses onset, none has
a right-censored luteal phase, and none has a calendar gap. A dataset in
which nothing unusual happens cannot show a setting working, and cannot
catch a claim about one that is wrong.
The fifteen cases
Figures are from a run at the default settings unless a setting is named. "Days covered" counts rows with a non-missing value, out of all rows for that person after the package fills the calendar.
- 1. Ordinary
Three complete cycles, ovulation confirmed, every phase in range. The baseline: without it you cannot tell a setting that recovered nothing from a dataset that is broken.
- 2. Eight rated days before the first recorded onset
With
impute_leading_ovulation = TRUE,cyclic_time_imputegains 15 days – but 7 of those rows are ones the package fabricated, because the imputed ovulation lands a week before this diary begins. Gains more rows than data, which is the caution that matters for this setting.- 3. Thirty rated days before the first onset
The same setting gains 15 of those 30 days and fabricates nothing. The 15 is a placement, not a cap, which this person is here to show:
leading_ovulation_luteal_days = 25scales 25 days,= 40scales 40 and fabricates 10 rows before the diary begins, and the phase-length caps never gate any of it (a 25-day imputed luteal phase scales withluteal_phase_max_daysleft at its default of 18). So this setting has no upper bound of any kind – not the cycle-length bounds, which need a cycle length there is none of, and not the phase caps. What limits it is the number you pass.- 4. A confirmed ovulation among the leading days
Nothing is imputed (the setting declines when a confirmed ovulation is already there), and those days scale anyway, in the confirmed-only columns, with no setting switched on. The six days before that ovulation have no onset on their left, so no follicular fraction exists for them and they stay empty.
- 5. A confirmed ovulation at the end, no closing onset
With
impute_next_menses = TRUE, the cycle closes 14 days after that ovulation: 14 days are gained incyclic_timeas well ascyclic_time_impute, and 5 rows are fabricated to reach the imputed onset.- 6. An 18-day cycle, ovulation not confirmed
Shorter than
lower_cyclength_bound, so no ovulation is imputed and the cycle has no cycle time at all – while the 28-day cycles either side of it do.- 7. A 42-day cycle, ovulation not confirmed
Longer than
upper_cyclength_bound, same result. Widening the two bounds toc(18, 43)imputes an ovulation for this cycle and person 6's, and recovers 58 days between them.- 8. Confirmed ovulation 22 days before the next onset
The luteal phase runs past
luteal_phase_max_days, inside a 35-day cycle that is still within the cycle-length bounds.cyclic_timecovers 14 days;cyclic_time_imputecovers 36, recovering the luteal phase through the phase-cap fallback and flagging 21 of those rows incyclic_time_impute_extended_phase.luteal_phase_max_days = 25returns all 22 days tocyclic_timeitself.- 9. Confirmed ovulation 26 days after the onset
The same, on the follicular side of
follicular_phase_max_days: 10 days incyclic_time, 36 incyclic_time_impute, 26 flagged.follicular_phase_max_days = 30returns all 26.- 10. Confirmed ovulation 5 days before the next onset
Under
luteal_phase_min_days, and this is the asymmetry: the fallback ignores the phase ceilings but still applies the floors, so unlike people 8 and 9 these days are empty in every column. Only loweringluteal_phase_min_daysrecovers them.- 11. An eleven-day hole in the diary
Eleven days with no rows at all, swallowing neither an onset nor an ovulation. They come back in the output with no rating on them, which is why a coverage figure counted over output rows overstates what you have.
- 12. Ovulation confirmed on no day
Every cycle in range, so every cycle takes the imputed path:
cyclic_timeis empty for this person andcyclic_time_imputecovers 86 days. The confirmed-versus-imputed contrast, in one person.- 13. No onset and no confirmed ovulation
No anchor of any kind. Nothing scales, under any setting. The case that reads as a bug.
- 14. A confirmed ovulation but no onset ever recorded
The menses-to-menses counter never starts, so there is no cycle length – and that one day still scales, pinned at
cyclic_time = 1, because the ovulation anchor is set independently of any cycle. Withimpute_next_menses = TRUEa closing onset is imputed and the whole luteal phase scales, 15 days, in the confirmed columns too.- 15. Both opt-in rules at once, at opposite ends
Ten rated days before the first onset and a confirmed ovulation at the end with no closing onset. The only person who exercises both rules together, and therefore the only one where the order they are applied in could matter. It does not: 15 days at the front, 14 at the back, 29 together, with rows fabricated at both ends.
Overlaps, and where the order of operations matters
pacts_scaling() applies impute_next_menses first and
impute_leading_ovulation second, so the leading rule can see a
table that already contains an imputed onset. Four things follow, all
measured on this dataset rather than reasoned from the code:
The two rules do not interact. They act on opposite ends of a diary, and on person 15, who has both, the days gained together are exactly the sum of the days each gains alone. The order is therefore not something a caller has to think about.
An imputed onset never becomes a leading-day anchor, and this holds structurally rather than by luck.
impute_next_mensesonly ever imputes an onset forward from a confirmed ovulation, so whenever that imputed onset is a person's first – person 14, who has no observed onset at all – there is by construction a confirmed ovulation before it, which is precisely the condition on whichimpute_leading_ovulationdeclines. No combination of the two settings imputes an ovulation before an imputed onset.leading_ovulation_luteal_daysis ungated in both directions, which person 3 demonstrates: nothing bounds how many leading days it will scale except the number you pass. Treat a value above the default as a methods decision to record, not a recovery setting to turn up.Persons 8 and 9 depend on a bound they do not demonstrate. The phase-cap fallback that recovers them is itself gated on the cycle falling inside
[lower_cyclength_bound, upper_cyclength_bound], and both their cycles are 35 days – the default upper bound exactly. Atupper_cyclength_bound = 34their recovered days drop from 72 to 24 and the extended-phase flag from 47 to 0, so they stop demonstrating the phase caps at all. The CLEAR lab's own[20, 43]window keeps them working; a narrower one silently changes what they show.
Three overlaps are deliberate and not conflicts. Persons 5 and 14 both
exercise impute_next_menses, but on 5 the imputed onset closes a cycle that
already had an opening onset, while on 14 it creates the only onset that
person has. Person 12 is what the whole dataset looks like under
ovtoday = NULL, which is worth having in one person as well as one call.
And person 10 exists to contradict persons 8 and 9: the phase-length floors
and ceilings are not symmetric, and only the ceilings have a fallback.
See also
cycledata for the ordinary practice dataset, pacts_scaling()
for the settings each person exercises.
Examples
# What each person is, and how much of them scales at the defaults
scaled <- pacts_scaling(
cycledata_special,
id = id, date = daterated, menses = menses, ovtoday = ovtoday
)
# Two settings, and the days each one gains
leading <- pacts_scaling(
cycledata_special,
id = id, date = daterated, menses = menses, ovtoday = ovtoday,
impute_leading_ovulation = TRUE
)
# People 2, 3 and 15 gain 15 days each; nobody else changes
gained <- function(x) tapply(!is.na(x$cyclic_time_impute), x$id, sum)
gained(leading) - gained(scaled)
#> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#> 0 15 15 0 0 0 0 0 0 0 0 0 0 0 15
# And the rows the package added, which carry no rating
sum(is.na(leading$symptom)) - sum(is.na(scaled$symptom))
#> [1] 12