get_procedure_codes() returns codes again. It errored with object '.cms_codes' not found for every format and every version, in the working tree and in the released 0.1.1 alike, so no supported call worked at all. The data had been saved with internal = FALSE, which writes data/.cms_codes.rda -- a dot-prefixed file that git add data/*.rda silently never matches, since a shell glob skips leading dots -- and the copy that had been living in R/sysdata.rda was evicted when the MAUDE annex codes were later saved from their own script, taking that file from 530,220 to 63,468 bytes. usethis::use_data(internal = TRUE) rewrites R/sysdata.rda wholesale rather than adding to it, so every internal dataset now has to be named in one call, and that call lives in the new data-raw/sysdata.R. The object is cms_codes rather than .cms_codes; nothing referenced it by name outside the accessor.
Rebuilding those tables turned up three parsing faults that had been bundled with them. 13 of the 1,170 CPT codes were NA: vroom::vroom() took the code column for a double from the numeric rows that lead the file, so the Category III codes, which end in T, failed to parse -- 0051T, cardiac, and 0585T, biliary, among them. The row count was unchanged either way, so the table looked complete while those procedures could not be looked up at all. 901 ICD10 codes were stored with the padding of the fixed-width field they were cut from, as "001 " rather than "001", and matched nothing a caller would write. 79 HCPCS descriptions carried literal quote characters, reading "\"Fluzone vacc, 3 yrs & >, im\"", the quotes having been stripped from the code alone. The generator now checks its own output for missing, empty and untrimmed codes before saving, since a parser that drops codes leaves the row count untouched.
get_procedure_codes() accepts format case-insensitively, as it has always been documented to. The check was a bare %in% against lowercase names, so get_procedure_codes("ICD9", 2014) was refused as an unsupported format.
maude_query() now returns the device problem terms it promises. The device_problem column was read from device[].device_problem_codes, which the openFDA device event endpoint never returns; the terms are at the top level of the record, in product_problems. Nothing errored and nothing warned, so the column arrived present, typed, and NA for every row of every query ever run, which reads as "MAUDE does not code device problems for these reports" rather than as a bug. In a 41,403-report extract of pulsed-field ablation reports it was empty throughout and the malfunction-mechanism analysis was nearly abandoned on that basis. Note the asymmetry that caused it: device.device_problem_codes is a valid field to search on, so the device_problem argument was always filtering correctly on a field that is never returned.
maude_codes() no longer gives 26 of the 27 Annex A families the wrong term. The bundled table was built by filling the level columns downward, which carries the previous family's last child into the next family row -- a family's Level 2 cell is blank because a family has no Level 2 term, not because the value is missing. A02 was therefore returned as "Implant subsidence" rather than "Manufacturing, Packaging or Shipping Problem". FDA codes a large share of reports at the family level, so joining returned MAUDE terms to this table on term silently dropped them: measured on the same pulsed-field extract, leaf-only matching left 3,466 of 11,683 device problem mentions unmatched. The hierarchy is now read from the IMDRF code, which spells the ancestry out exactly, and level_1 is populated for every row of every annex. It was previously missing on 783 of 807 Annex E rows and 43 of 72 Annex F rows, so those two annexes had effectively no hierarchy at all.
maude_query() no longer repeats each coded term. openFDA emits the product_problems and patient.patient_problems arrays twice for most records, an artefact of how the endpoint joins its tables, so a single-device report came back as "Pericarditis; Pericarditis". Narrative blocks are still returned in full: each carries its own mdr_text_key and a supplement may legitimately repeat the text of the report it amends.
cosinor_reg() can be fitted through parsnip again. The engine registration never called parsnip::set_encoding(), and since parsnip::get_encoding() returns NULL for a missing table rather than raising an error, the defaults it would otherwise have supplied never applied and the fit failed inside vctrs with a message naming neither the model nor the engine. A specification still built and printed correctly, so nothing was visibly wrong until something was fitted with it. The registration is also no longer skipped wholesale once the model exists -- only set_new_model() is guarded now, the remaining setters being idempotent -- so an incomplete registration repairs itself on the next load instead of persisting for the life of the session. The documented example now fits rather than stopping at the specification, and the test for it is no longer skipped.
confint() on a population-mean cosinor no longer reports amplitude and acrophase intervals that are too wide by a factor of the square root of the number of subjects. The mesor variance was divided by the subject count while the amplitude and acrophase variances were not, so the 95% interval for the amplitude of the 24-hour rhythm in twins came back as -0.399 to 0.975 -- an interval covering zero, and impossible values below it, for a rhythm measured across 741 subjects. It is 0.263 to 0.313.
The acrophase standard error carried the wrong sign on its cross term. The delta method gives the amplitude a negative cross term and the acrophase a positive one, and both were written negative. The error is invisible whenever time is sampled evenly across whole cycles, since it acts only through the covariance of the two regression coefficients, which is why the bundled data never showed it; on an unevenly sampled design it inflated the standard error by around 37%.
cosinor_zero_amplitude() now scales with the number of components. It fixed the numerator at 2 degrees of freedom and the denominator at N - 3 whatever tau was given, so a two-component model on twins reported F = 707 on 2 and 16383 degrees of freedom where the test is F = 354 on 4 and 16381. It also returns a p-value.
Fitted values and residuals from a population-mean cosinor are now aligned with the rows they came from. They were returned in subject order while the outcome stayed in input order, so both were correct only when the data happened to arrive sorted by subject.
cosinor() now accepts non-numeric subject identifiers in population. The insufficient-observations filter coerced the subject names with as.numeric(), which made every one of them NA for character identifiers, so no subject was dropped and the per-subject fit then failed on a singular matrix.
cosinor_features() no longer scrambles the components of a multiple-component population model when reconstructing its fitted curve, and its harmonic check now considers every period rather than only the longest and shortest, so tau = c(24, 5, 12) is correctly reported as non-harmonic.
maude_adjudicate() and maude_term_to_complication() work when the package is loaded but not attached.
Their bundled tables were named bare as argument defaults, which resolve through the search path, so card::maude_term_to_complication("Cardiac Tamponade") without a preceding library(card) stopped with object 'complication_definitions' not found.
The defaults now name the package.
Ten of the 37 corporate ownership rows carried a wrong closing date, found by checking every one of them against the acquirer's own announcement or its SEC filing. Two were wrong in a way that hides reports. Cardiac Science was recorded as ZOLL's from 2016 when ZOLL did not close the acquisition until 2019-08-26, and the company that owned it in between, Opto Circuits, was absent from the table entirely, so three and a half years of reports resolved to the wrong company. Cordis was recorded as Johnson & Johnson's until 2021, when J&J had sold it to Cardinal Health on 2015-10-02 and Cardinal sold it on to Hellman & Friedman on 2021-08-02, so six years of catheter reports named a company that no longer owned the business. The others were off by between a day and eleven months: Datascope (2008-11-01, actually 2009-01-30), EPiX Therapeutics (2018-11-01, actually announced 2019-01-24), nContact Surgical (2015-01-01, actually 2015-10-13), Irvine Biomedical (2004-01-01, actually 2004-10-07), Maquet (2000-01-01, actually 2000-11-17), HeartSine Technologies (2015-06-01, actually September 2015), Galil Medical (2016-08-01, actually mid-June 2016) and ev3 (2010-07-13, actually 2010-07-12). Every dated row now carries the source its date came from, or says in its note how approximate the date is, and a test refuses a dated row that does neither.
Biosense Webster now resolves to Johnson & Johnson rather than to itself. It had been left terminal on the rule that the chain stops at the operating company a report would name, but Abbott and Cordis reports already rolled up to J&J, so a query for Johnson & Johnson's cardiac devices found Abiomed and missed every Biosense Webster catheter.
query_genetic_variants() refuses max_results above 10,000 up front, since NCBI E-utilities refuse to page past that record and the pull would otherwise fail part-way through.
maude_query() returns four more fields: date_of_event, report_source_code, reporter_country_code and mfr_report_type. Each answers a question the eleven columns it returned before could not. date_of_event gives the reporting lag -- median 23 days and mean 44 in a 200-report sample of pulsed-field ablation catheters, with a tail to 390 -- which is what any analysis anchored to something that happened in the procedure room rather than in the mailroom needs, since a quarter of reports received in the month after a labelling change describe procedures performed before it. report_source_code separates the mandatory manufacturer stream from the voluntary one, 95% against 3% in the same sample. reporter_country_code shows that only about half of that stream is US, which decides whether a US regulatory action could have reached the reports being counted.
Two fields that would have been the obvious choices are not returned, because openFDA serves them empty: event_key, documented as the link between an initial report and its supplements, was blank for every one of 300 records tested, and event_location for all but 1.7%. mfr_report_type is returned in place of the first and marks a filing as "Follow-Up" without naming what it follows, which supports dropping follow-ups as a sensitivity but not a true de-duplication. reporter_country_code stands in for the second. Both substitutions are recorded here rather than only in the code, since "the field for this is served empty" is a stronger limitation than "we did not have it".
maude_normalize_manufacturer() maps MAUDE's manufacturing entities onto a canonical name.
manufacturer_name is the plant, not the company, so counting the raw strings splits Biosense Webster across four plants plus STOCKERT GMBH, divides Boston Scientific's pulsed-field reports between BOSTON SCIENTIFIC CORPORATION and FARAPULSE, INC., and hides Abbott behind VENUSA DE MEXICO S.A. DE C.V., a contract manufacturer.
A string the registry does not cover returns NA rather than a guess.
maude_resolve_owner() answers the separate question of who owned an entity, and when.
Ownership is dated rather than a company name baked into each alias, which is what makes an acquisition one row: "Telectronics Pacing Systems" reaches Abbott through St. Jude Medical without anyone writing Abbott next to Telectronics, and acquiring a parent carries every entity beneath it.
It also expresses a divestiture, which a single-parent column cannot — an entity with no row matching the date owns itself, so Physio-Control resolves to Medtronic in 2005, to itself in 2014, and to Stryker in 2020 — and an entity sold on twice, so Cordis resolves to Johnson & Johnson in 2010, to Cardinal Health in 2018, and to Hellman & Friedman today.
as_of has deliberately no default: pass Sys.Date() for the company that owns the entity now, or a report's date_received for the company that owned it when the report was filed.
A Sys.Date() default would have made the same code return different answers after a future acquisition without anything in the call having changed.
maude_search_terms() runs the same registry backwards, from a company to every name its reports have been filed under, which is the direction that stops a query hiding reports.
Normalising happens after the reports are in hand and so cannot help with the reports a query never asked for: a filter on "Abbott" reaches nothing St. Jude Medical filed before 2017, and a result missing twenty years of reports looks exactly like one that is not.
It returns the terms with the window each was in use, so maude_query(manufacturer_name = maude_search_terms("Abbott")$term) asks for the family rather than the current name, and as_of narrows the set to a point in time.
maude_query() warns when a manufacturer filter names a company whose other names are being left out, saying which ones and pointing at maude_search_terms().
It does not expand the query, since the query sent should stay the query written.
maude_normalize_ablation() derives the ablation platform, energy modality and maker from the brand name.
Product code does not determine modality — OAE covers cryoablation and radiofrequency alike — so a rule resolving it from the product code and falling back to radiofrequency puts cryoballoon, pulsed-field and laser devices in a radiofrequency arm.
Two things it is careful about: "arctic" as a substring is not a cryoablation signal, since ARCTIC SUN is a temperature management console, and the mapping, access and irrigation devices that share the ablation product codes (PENTARAY, OCTARAY, FARADRIVE, RHYTHMIA, ENSITE) are matched but carry no modality rather than being swept into an arm.
The table names the entity that makes each platform, never its corporate parent, so it does not have to be edited when a company is acquired.
The facts behind all of these are maude_entities() and maude_platforms(), two registries in one file, replacing four CSVs under data-raw/, the script that rebuilt them, and three bundled datasets.
There are 61 entities and about 110 facts, which is small enough that adding a company should be an edit rather than an edit, a sourced script, a regenerated dataset and a rebuild.
Each entity carries its aliases as literal search terms rather than as a regular expression, which is what lets the same column be searched on and matched against, and each dated row cites the announcement its closing date came from.
Precedence is no longer a hand-maintained priority column: where a string matches two entities, the one below the other in the ownership graph wins, so "MEDTRONIC CRYOCATH LP" is CryoCath because Medtronic is its parent.
maude_entity_coverage() measures how much of MAUDE the registry can name, against the openFDA count endpoint, replacing the coverage figures that used to be stored beside the data and go stale on the next edit.
maude_fda_api_call() gains a count argument, reaching the openFDA count endpoint. It aggregates on the server and returns a two-column tibble of term and count, so a device problem frequency table over 41,000 reports takes one request rather than roughly 414 paginated ones. Two things it will not tell you, both documented in ?maude_query: the endpoint caps at 1000 terms with no pagination cursor, and the counts are of mentions rather than of reports. limit, skip and api_key now have defaults, so a count call needs only the query and the field.
maude_query() warns when the query matched more reports than limit returned, and carries the matched count on the result as a "total" attribute. limit is a cap on the call, and a query matching 41,000 reports and a query matching exactly limit of them previously came back looking identical.
maude_adjudicate() records what produced an adjudication, as "adjudication_model", "prompt_hash" and "definitions_hash" attributes on the returned list. The internal system prompt and complication_definitions will both change, and a dataset adjudicated under one pair is not comparable to a dataset adjudicated under another, but nothing in the flags themselves said which was used.
cosinor() objects gain the standard extractor methods -- coef(), vcov(), sigma(), nobs(), df.residual() and logLik(), and through the last of these AIC() and BIC(). Every statistic the package reports is now derived from vcov() rather than rebuilding the covariance matrix in each function, which is what let the acrophase sign error survive next to a correct copy of the same formula. coef() and vcov() take a type argument for either the regression coefficients or the amplitude and acrophase parameterisation.
confint() returns a matrix and honours parm, as the stats generic requires, rather than a list of intervals and standard errors. An unrecognised parm is an error naming the parameters the model does carry, instead of a silent NA row. Standard errors now come from vcov(). This is a breaking change to the return shape, made without a deprecation window because the values it returned for the acrophase and for every population amplitude were wrong.
confint() gains method = "ellipse", giving the conservative limits derived from the joint confidence region for each component rather than a symmetric interval around the estimate. These respect the parameter space: an amplitude bound cannot fall below zero, and where the region covers the pole the acrophase is reported as unidentifiable rather than as an interval. cosinor_area() now returns those limits and a covers_pole flag, having previously computed them and returned only the plotting coordinates, and takes a component argument instead of always describing the first.
anova() on a cosinor tests each component for a non-zero amplitude on 2 degrees of freedom, which is the question a multiple-component model raises and the package had no way to answer -- a user fitting tau = c(24, 12) could read off a 12-hour amplitude but not ask whether it was distinguishable from noise. Given several models it compares them sequentially. glance() summarises a fit in one row.
cosinor_order() fits a nested family of harmonic models and reports the sequential F test and the information criteria for each, so the number of components can be chosen rather than assumed.
tidy() gains statistic and p.value columns. The statistic is the per-component F test, so a component's amplitude and acrophase share it: the null being tested is that both regression coefficients are zero, and an acrophase has no meaning under it.
cosinor() now refuses periods it cannot fit -- duplicated, non-positive, non-finite, or more parameters than observations -- and warns when the periods given cannot be separated by the data, reporting the design condition number. tau = c(24, 23.5) on twins returns amplitudes of 7.4 and 7.1 against a single-component amplitude of 0.30, because two near-collinear components can grow without bound so long as they cancel; this previously happened silently. See ?cosinor_identifiability for why the condition number is used in preference to the usual spectral resolution criterion, which rejects the package's own tau = c(24, 12) example on folded clock time.
cosinor_goodness_of_fit() and cosinor_area() now refuse population-mean models rather than returning a statistic with a note that it may be inaccurate. Neither quantity is defined for a pooled per-subject fit, and both would have printed a plausible number.
Every user-facing MAUDE function now starts with maude_, so the family tab-completes together: query_maude() is maude_query(), adjudicate_maude_event() is maude_adjudicate(), load_maude_codes() is maude_codes(), and the three entity lookups are maude_normalize_manufacturer(), maude_normalize_ablation() and maude_resolve_owner(). The old names are removed rather than kept as aliases; normalize_maude_terms() is no longer exported.
maude_adjudicate() refuses a bad argument before anything is sent to the model. Passing chat_object = NULL previously died inside the function with attempt to apply non-function, which named neither the argument nor the fix. delimiter has moved to the end of the argument list, so the three required arguments come first.
maude_codes() and maude_search_terms() accept their name in any case, so maude_codes("a") and maude_search_terms("abbott") work. An unknown company is reported with the registry's closest names where there are any, and without an empty clause where there are not.
Internal helpers no longer have their own help pages. The 22 pages such as ?cosinor_jacobian and ?.parse_csq_field are gone; the same text stays as comments above each function in the source.
maude_fda_api_call() explains an HTTP 403 rather than passing openFDA's message through. openFDA answers any refused anonymous request with "No api_key was supplied", which is what it says whether the per-minute rate was exceeded or limit was set above 999 -- neither of which needs a key to fix.
maude_codes() documents the annex hierarchy and the polyhierarchy in Annex E, where a term such as "Brain Injury" belongs to two families and so appears on one row per parent. That is correct, and it will double-count if a join on term is then tallied without reducing to imdrf_code first.
The extract_*() echocardiogram functions are now vectorized over text and return one element per report. extract_echo_findings() accordingly returns a tibble with one row per report instead of a list.
extract_la_size() gains a range argument for graded descriptions such as "mildly to moderately dilated", defaulting to the upper end of the range. Grades are normalized ("mildly" becomes "mild"), and "not well seen" is now reported as a missing size plus a separate la_not_visualized flag rather than being returned as if it were a size.
extract_lvef() now captures values written with an inequality, such as <20%, and tolerates phrasing between the term and the value ("ejection fraction by visual estimate is 55%"). extract_echo_findings() reports the inequality in a new lvef_qualifier column.
extract_lvidd() no longer reads a measurement belonging to another structure. It previously took the first number to follow the term no matter how far away, so a report such as "LVIDd: not measured. LA A/P 4.3 cm" was given the left atrial diameter. It now reads only within the same clause, excludes the left atrium by name, and skips stray digits, which also makes the millimeter conversion reliable ("LVIDd 52 mm" is 5.2 cm).
extract_lvef() and extract_lvidd() gain the min_val and max_val arguments that extract_la_diameter() already had, so the plausible range for each measurement can be set by the caller. extract_lvidd() now applies such a range at all, defaulting to 1 to 10 cm.
Linear dimensions written without units are now resolved against that range instead of being assumed to be centimeters. Structured fields frequently omit the units, and a chamber dimension cannot be plausible in both centimeters and millimeters, so "LVIDd: 52" and "LA A/P: 43" resolve to 5.2 cm and 4.3 cm. Units the report does write are still taken at face value, so "LVIDd 52 cm" remains missing rather than being quietly reinterpreted.
extract_la_diameter() no longer loses a measurement to its own decimal point. Its fallback keyword search split report text on ., which also split "4.5" in two and left neither piece holding both the keyword and the value, so that search could only ever match whole numbers. It also now recognizes measurements written in millimeters.
Severity and diastolic dysfunction grades in extract_echo_findings() are now found whether the grade precedes or follows the structure ("mild mitral regurgitation" as well as "mitral regurgitation is mild"), take the upper end of graded ranges, and match valve-inclusive terms such as "mitral valve regurgitation". The ambiguous as and ar abbreviations were dropped, as they matched ordinary English.
cosinor_order() has chosen the harmonic order are anticonservative, since the selection looked at the same data. This is an open problem in the cosinor literature rather than something the package currently solves.procedure_codes() is renamed to get_procedure_codes(), following the package's own get_*() convention for accessors. The old name reads as a dataset and tab-completes next to complication_definitions and maude_complication_index, which are data, while it is in fact a function of format and version. The old name still works and warns once per session rather than once per call, since these are looked up in a loop over codes.
cosinor() and its companion functions -- cosinor_features(), cosinor_area(), cosinor_goodness_of_fit(), cosinor_zero_amplitude(), cosinor_order(), cosinor_reg(), ggcosinor(), ggellipse() and the cosinor methods -- will leave before version 1.0, for a separate package for recurring and periodic events that does not yet exist. Nothing changes in this release beyond the notice on ?cosinor; the deprecation warning and the removal will each get their own release.
The longitudinal event functions are being moved to a separate package to make maintenance more straightforward.
cosinor() now has a stable population mean cosinor option with appropriate confidence intervals
procedure_codes() has the latest ICD10 codes, as of 11/2023, and are included in the package
The circadian-rhythm features have been deprecated and recurrent data features have been removed
The cosinor() functions will be updated to be more customizable and more efficient, however will be moving to a separate package by v0.2.0
cosinor() unable to run on certain models based on y valuescosinor_features() allows for assessing global/special attributes of multiple component cosinor analysisggcosinor() is now functional for single and multiple component analysisbuild_sequential_models(), however it is in a list format and will likely be updated to be more "tidy" in the futureggpopcosinor() can show the cosinors for individuals across a population, along with mean and predicted cosinorggcosinor() accepts single modelsprint.cosinor() and plot.cosinor() functions addedcosinor_zero_amplitude() test added, works for individual cosinor.cosinor() now takes the argument
of for individuals. The individual cosinor methods generally work, but may not
yet be accurate.circ_compare_groups() helps to summarize circadian data by an covariate and
time. This is visualized using ggcircadian(). Also includes the ggforest()
to create forest plots of odds ratios. This is dependent on the circ_odds()
function to generate odds ratios by time.hardhat package from tidymodels, cosinor() introduced
as a new function to allow for diagnostic analysis of circadian patterns.
Although the algorithm is well known, having an implementation in R allows
potential diagnostics. This includes the ggcosinorfit() allows for assessing
rhythmicity and confidence intervals of amplitude and acrophase of cosinor
model. Basic methods for assessing the model, such as print, summary,
coef, and confint currently function.recur_survival_table(), which allows for redesigning longitudinal data tables
into a model appropriate for analysis. It is built to extend survival analyses.
The recur_summary_table() function allows for reviewing the findings from
recurrent events by category to help understand event strata.circ_sun() function allows for identifying the sunrise and sunset times
based on geographical location. This is intended to couple with the
circ_center() function to center a time series around an event, such as
sunrise. A vignette has been added to review this data.