Skip to main content
← Back to Software

tvtools

Cohorts, Registries & Time-Varying Data

Prescription and episode records to time-varying survival data

. net install tvtools, from(...)
View on GitHub →

tvtools hex logo

Version 1.18.2 | 2026-10-26

version Stata 16+ MIT license Part of Stata-Tools

Quick Start · Installation · Commands · Worked Examples · Stata-Tools · Report a bug

tvtools turns person-level follow-up and episode records into analysis-ready time-varying survival data. It gives applied survival analysts transactional builds, composable interval primitives, diagnostics, weighting, fixed-width panels, and exact calendar-timescale splitting.

Quick Start

After installing, turn raw dispensing records into exposure intervals and draw them for the first 12 people. The block simulates a 200-person cohort with SSRI and SNRI dispensings, exactly as the demo does:

clear
set seed 20260629
set obs 200
generate long id = _n
generate int study_entry = mdy(1, 1, 2015) + int(runiform() * 365)
generate int study_exit = study_entry + 365 + int(runiform() * 1460)
format study_entry study_exit %tdCCYY/NN/DD
generate byte female = rbinomial(1, 0.55)
generate double age = 40 + int(runiform() * 30)
tempfile cohort episodes
save `cohort'

* Raw dispensing records: up to four episodes per person, SSRI or SNRI
expand 1 + int(runiform() * 4)
bysort id: generate int seq = _n
bysort id: generate int duration = 60 + int(runiform() * 300)
bysort id: generate int rx_start = study_entry if seq == 1
bysort id: replace rx_start = rx_start[_n-1] + duration[_n-1] + 1 + int(runiform() * 60) if seq > 1
generate int rx_stop = rx_start + duration
format rx_start rx_stop %tdCCYY/NN/DD
generate double p_exposed = invlogit(-1 + 0.02 * age + 0.3 * female)
generate byte drug = 0
replace drug = 1 + int(runiform() * 2) if runiform() < p_exposed
label define drug_lbl 0 "Unexposed" 1 "SSRI" 2 "SNRI"
label values drug drug_lbl
drop if drug == 0
keep id rx_start rx_stop drug
save `episodes'

use `cohort', clear
tvexpose using `episodes', id(id) start(rx_start) stop(rx_stop) exposure(drug) reference(0) entry(study_entry) exit(study_exit) keepdates
tvdiagnose, id(id) start(rx_start) stop(rx_stop) exposure(tv_drug) swimlane maxids(12)

Exposure swimlane for 12 people: one horizontal lane per person across 2015 to 2019, coloured by period as Unexposed, SSRI, or SNRI, with each lane running from study entry to study exit

Drawn by demo/demo_tvtools.do, which also sets the white_tableau scheme, creates a dob variable in the cohort, and writes the episodes to a named file instead of a tempfile.

Features

  • Every day of follow-up accounted for. Intervals are closed, inclusive Stata daily dates, [start, stop], that tile each person's follow-up with the reference category filling uncovered time; stset maps them with time0(start - 1).
  • One validated call, or every step in view. tvbuild preflights the plan, builds through the same engines as the primitives, and commits the output frame and a provenance manifest as one transaction; dryrun validates without committing. tvexpose → tvmerge → tvevent keeps each decision separate.
  • Identifiers are never guessed. Source and master IDs must agree in name and storage type; commands never pad, recast, or silently remap them.
  • Quantities that add up. rate(), total(), and cumulative() variables follow their own rules when intervals are split, so totals are apportioned by inclusive days and still sum to the source.
  • Lost person-time is reported. tvmerge counts days present in some inputs but not all in r(n_persontime_lost), and tvage counts persons dropped outside minage()/maxage() in r(n_persons_dropped).
  • Diagnostics and weights in the same suite. tvdiagnose reports coverage, gaps, overlaps, and exposure summaries and draws swimlanes; tvweight estimates IPTW and IPCW weights with balance, effective sample size, and positivity diagnostics.

Requirements

  • Stata 16 or later.
  • No required community package.
  • Optional psdash for tvweight, loveplot; weighting, balance output, and r(balance) do not require it.
  • Optional msm for downstream msm_prepare and msm_weight workflows after tvpanel.

Installation

Install the released package from Stata-Tools:

capture ado uninstall tvtools
net install tvtools, from("https://raw.githubusercontent.com/tpcopeland/Stata-Tools/main/tvtools") replace

Install optional integrations only when those workflows are needed:

net install psdash, from("https://raw.githubusercontent.com/tpcopeland/Stata-Tools/main/psdash") replace
net install msm, from("https://raw.githubusercontent.com/tpcopeland/Stata-Tools/main/msm") replace

The ancillary menu helper is available with net get tvtools; run do tvtools_menu_setup.do only if you want to add its Stata menu entry.

Commands

Command Purpose
tvtools List and categorize the suite
tvbuild Build a committed interval frame from a cohort and one or more sources
tvspec Create and inspect the multi-source specification frame consumed by tvbuild
tvexpose Convert raw episodes or point-time records into time-varying exposure intervals
tvmerge Align two or more interval datasets and combine exposure quantities
tvevent Add single, competing, or recurrent events to interval data
tvdiagnose Report coverage, gaps, overlaps, exposure summaries, and swimlanes
tvweight Estimate treatment and censoring weights and report balance and overlap diagnostics
tvage Expand one row per person into exact calendar-age intervals
tvband Split existing intervals on one age, calendar, or elapsed-time axis
tvsplit Split existing intervals on several Lexis timescales at once
tvpanel Build a fixed-width, entry-anchored person-period grid for MSM work

How It Works

tvbuild is the recommended front door for a cohort plus one or more longitudinal sources: it normalizes a one-source shortcut or a typed tvspec frame, performs a read-only preflight, constructs intervals through the shared engines, optionally integrates events, and commits the destination frame and provenance manifest transactionally.

The primitive workflow is tvexpose → tvmerge → tvevent → tvdiagnose and/or tvweight. Use the primitives when the exposure definition, overlap rule, quantity algebra, or alignment must remain visible as a separate analytical decision.

Every interval in the construction commands is a closed, inclusive interval of integer Stata daily dates, [start, stop]. Abutting rows satisfy next_start = prior_stop + 1; a shared date is an overlap, not an abutment.

tvage, tvband, and tvsplit add exact birthday, calendar, and elapsed-time axes, while tvpanel creates a uniform entry-anchored grid and can carry cumulative exposure history into each period.

Choosing a Workflow

Need Use Result
One or several sources with a committed, auditable destination tvspec + tvbuild Named output frame, optional manifest, stage counts, and a verified signature
An advanced exposure rule or custom interval alignment tvexpose, tvmerge, and tvevent Explicit primitive stages that can be inspected or chained through frames
A uniform panel for time-varying treatment models tvpanel, then tvweight Entry-anchored periods, active class, and optional cumulative histories
Age, calendar, or follow-up timescale splitting tvage, tvband, or tvsplit Inclusive sub-intervals with exact calendar boundaries

Worked Examples

1. From cohort to stset in one tvbuild call

This complete example creates its own cohort and episode data, so it runs after installation from any working directory without the setup block.

clear
input long id study_entry study_exit
1 21915 22280
2 21915 22280
end
format study_entry study_exit %td
tempfile cohort episodes
save `cohort'

clear
input long id rx_start rx_stop byte rx_class
1 21919 21965 1
1 22024 22069 2
2 22100 22151 1
end
save `episodes'

use `cohort', clear
tvbuild, sourceusing(`"`episodes'"') id(id) entry(study_entry) exit(study_exit) ///
    start(rx_start) stop(rx_stop) exposure(rx_class) reference(0) ///
    generate(tv_drug) frameout(analysis) replace
frame analysis: tvdiagnose, id(id) start(start) stop(stop) ///
    entry(study_entry) exit(study_exit) all
frame change analysis
generate byte _failure = 0
generate double analysis_t0 = start - 1
stset stop, id(id) failure(_failure) time0(analysis_t0)

tvbuild leaves the person-level master unchanged, commits the interval result in frame analysis, and creates analysis_manifest unless nomanifest is specified. The subtraction of one day maps the package’s closed [start, stop] intervals to Stata’s open-left survival-time convention.

Example 1 is self-contained. The setup block below creates reusable tempfiles for examples 2 to 8; run it once in the same Stata session before copying any one of those blocks. The setup block also returns to the default frame and drops the analysis frames left by example 1, so the later examples can follow it in the same session.

frame change default
capture frame drop analysis
capture frame drop analysis_manifest
clear
input long id study_entry study_exit byte female
1 21915 22280 1
2 21915 22280 0
3 21930 22280 1
4 21930 22280 0
end
format study_entry study_exit %td
tempfile cohort episodes episodes2 events recurrent
save `cohort'

clear
input long id rx_start rx_stop byte rx_class
1 21919 21965 1
1 22024 22069 2
2 22000 22040 1
3 21950 22030 2
end
save `episodes'

clear
input long id rx_start rx_stop byte rx_class
1 21930 21990 1
2 22050 22100 1
3 21970 22010 1
end
save `episodes2'

clear
input long id event_date death_date
1 22010  .
2     . 22100
3 22020  .
4     .     .
end
save `events'

clear
input long id hosp1 hosp2
1 22010 22100
2 22050     .
3 21980 22030
4     .     .
end
save `recurrent'

2. Plan and commit with tvbuild

dryrun validates the plan and changes nothing; the second call commits the same plan to a named frame and manifest.

use `cohort', clear
tvbuild, sourceusing(`"`episodes'"') id(id) entry(study_entry) exit(study_exit) ///
    start(rx_start) stop(rx_stop) exposure(rx_class) reference(0) ///
    generate(tv_drug) frameout(analysis) dryrun
tvbuild, sourceusing(`"`episodes'"') id(id) entry(study_entry) exit(study_exit) ///
    start(rx_start) stop(rx_stop) exposure(rx_class) reference(0) ///
    generate(tv_drug) frameout(analysis) manifestframe(provenance) replace
frame provenance: list stage source_name n_input n_output, noobs

3. Describe multiple sources with tvspec

tvspec appends one typed row per source and preserves row order; tvbuild applies the cross-source rules and performs the build.

tvspec create build_spec, replace
tvspec add build_spec, name(drug) using(`"`episodes'"') start(rx_start) stop(rx_stop) ///
    exposure(rx_class) generate(tv_drug) reference(0)
tvspec add build_spec, name(second) using(`"`episodes2'"') start(rx_start) stop(rx_stop) ///
    exposure(rx_class) generate(tv_second) reference(0)
tvspec list build_spec
use `cohort', clear
tvbuild, specframe(build_spec) id(id) entry(study_entry) exit(study_exit) ///
    frameout(analysis) manifestframe(provenance) replace

4. Keep a primitive workflow in frames

frameout() leaves the caller’s data intact, and frames() lets tvmerge consume named intermediate results without save/use round trips.

use `cohort', clear
tvexpose using `"`episodes'"', id(id) start(rx_start) stop(rx_stop) ///
    exposure(rx_class) reference(0) entry(study_entry) exit(study_exit) ///
    generate(tv_drug) frameout(f_drug) replace
tvexpose using `"`episodes2'"', id(id) start(rx_start) stop(rx_stop) ///
    exposure(rx_class) reference(0) entry(study_entry) exit(study_exit) ///
    generate(tv_second) frameout(f_second) replace
tvmerge, frames(f_drug f_second) id(id) start(rx_start rx_start) ///
    stop(rx_stop rx_stop) exposure(tv_drug tv_second) frameout(f_merged) replace
use `events', clear
tvevent, frame(f_merged) id(id) date(event_date) compete(death_date) generate(_failure)

5. Track dose history on a regular exposure scale

In dose mode, exposure() is the amount, reference() defaults to zero, and cumulative history is measured at the start of each output row.

clear
input long id study_entry study_exit
1 21915 22280
2 21915 22280
3 21930 22280
end
tempfile dose_cohort dose_episodes
save `dose_cohort'
clear
input long id rx_start rx_stop double dose_mg
1 21920 21950 10
1 21980 22020 20
2 22000 22060 15
3 21960 22030 5
end
save `dose_episodes'
use `dose_cohort', clear
tvexpose using `"`dose_episodes'"', id(id) start(rx_start) stop(rx_stop) ///
    exposure(dose_mg) entry(study_entry) exit(study_exit) dose ///
    generate(cum_dose) keepdates
tvdiagnose, id(id) start(rx_start) stop(rx_stop) exposure(cum_dose) summarize

6. Format recurrent events

For a wide event stub such as hosp1, hosp2, and so on, type(recurring) creates the event-sequence stratum and gaptime adds a reset clock.

use `recurrent', clear
tempfile recurrent_events
save `recurrent_events'
use `cohort', clear
keep id study_entry study_exit
rename study_entry win_start
rename study_exit win_stop
tempfile recurrent_intervals
save `recurrent_intervals'
use `recurrent_events', clear
tvevent using `recurrent_intervals', id(id) date(hosp) type(recurring) ///
    generate(hosp_event) start(win_start) stop(win_stop) enum(stratum) ///
    gaptime gapstart(t0) gapstop(t) timegen(tstop) timeunit(days)

7. Build an MSM panel and estimate weights

tvpanel emits every entry-anchored period, including reference periods, and tvweight can use its period index for a stabilized treatment model.

The synthetic exposure below covers the middle periods for every person, so the all-reference boundary periods are removed before fitting the illustrative treatment model; real analyses should retain only periods with support for the model they specify.

clear
set seed 20260805
set obs 40
generate long id = _n
generate double study_entry = 21915 + floor(runiform() * 20)
generate double study_exit = study_entry + 364
generate double age = 45 + 10 * runiform()
generate byte female = runiform() > .5
generate byte rx_class = runiform() < invlogit(-.4 + .04 * (age - 50) + .4 * female)
tempfile panel_cohort panel_episodes
save `panel_cohort'
keep id study_entry study_exit rx_class
generate double rx_start = study_entry + 40
generate double rx_stop = study_exit - 40
save `panel_episodes'
use `panel_cohort', clear
tvpanel using `"`panel_episodes'"', id(id) entry(study_entry) exit(study_exit) ///
    exposure(rx_class) start(rx_start) stop(rx_stop) width(91) ///
    period(period) generate(tv_class) keepvars(age female) replace
drop if period == 0 | period == 4
tvweight tv_class, covariates(age female) id(id) time(period) ///
    stabilized generate(iptw) nolog

8. Split one interval on several time axes

tvsplit accepts one or more axes and carries the other variables onto every resulting row. Variables tagged as interval totals (for example tvmerge, total() output) are apportioned by inclusive days instead, so the pieces sum to the source total (a band dropped by min() or max() takes its share with it); rates and row-start cumulative histories are copied unchanged.

clear
input long id double start stop dob entry
1 21915 22280  -3653 21915
2 21930 22280  1826 21930
end
format start stop dob entry %td
tvsplit, id(id) start(start) stop(stop) ///
    age(dob, width(5) generate(ageband)) ///
    calendar(, width(1) anchor(2020) generate(calband)) ///
    elapsed(entry, width(1) unit(year) generate(fuband))

Demo

Regenerate the checked-in figures and console assets by running demo/demo_tvtools.do from a repository checkout; the demo script creates its own synthetic data and is not part of the net install payload. It installs tvtools (and the white_tableau scheme from tc_schemes) from the neighbouring checkout directories, so run it from a full Stata-Tools checkout; it leaves those installs in your PLUS directory, and restores your graph scheme at the end.

local demo_dir "/path/to/checked-out/tvtools/demo"
do "`demo_dir'/demo_tvtools.do" "`demo_dir'"
Output Command focus
Exposure swimlane: 12 people's follow-up as horizontal lanes coloured Unexposed, SSRI, or SNRI tvexpose, then tvdiagnose, swimlane maxids(12)
Covariate balance love plot: standardized mean differences for age, female, and biomarker before and after weighting tvweight, ipcw() stabilized balance loveplot

Console reports from the same script, converted to Markdown with logdoc:

File What it shows
console_primitives.md tvexpose to frames, tvdiagnose, tvmerge, and tvevent without save/use round trips
console_tvbuild.md The same construction as one tvbuild call, with tvspec, dryrun, the manifest, and a cf check against the primitive route
console_msm.md tvweight with IPTW × IPCW, balance, and positivity diagnostics
console_recurrent.md tvevent, type(recurring) with an event-sequence stratum and gap-time clock
console_multigroup.md Multinomial tvweight and tvage age bands

Command Reference

tvtools

tvtools [, list detail category(string)]

list prints command names, detail prints descriptions, and category() filters to all (the default), prep, diag, or weight. It returns r(commands), r(n_commands), r(version), and r(categories).

Options

Option Purpose
list Print command names only
detail Print command names with descriptions
category(string) Select all (default), prep, diag, or weight

tvspec

tvspec create framename [, replace]
tvspec add framename, name(name) (frame(name) | using(filename)) ///
    start(name) stop(name) exposure(namelist) generate(namelist) ///
    [reference(#) kind(episodes|intervals) referencelabel(string) ///
     label(string) description(string) rate(namelist) total(namelist) ///
     cumulative(namelist)]
tvspec list framename

create writes the empty typed schema; add defaults kind(episodes), requires exactly one of frame() and using(), and maps exposure() to generate() positionally. Episode rows require reference(); interval rows use kind(intervals) and must not specify a reference. rate(), total(), and cumulative() are optional subsets of exposure().

tvbuild

tvbuild, specframe(name) id(varname) entry(varname) exit(varname) frameout(name) [options]
tvbuild, (sourceframe(name) | sourceusing(filename)) id(varname) entry(varname) exit(varname) ///
    start(name) stop(name) exposure(name) reference(#) generate(name) frameout(name) [options]

Options are sourcename(), referencelabel(), label(), startname(), stopname(), dateformat(), keepvars(), dropdates, coverage(), eventframe(), eventusing(), eventdate(), eventtype(), compete(), eventgenerate(), eventlabel(), timegen(), timeunit(), enum(), gaptime, gapstart(), gapstop(), manifestframe(), nomanifest, dryrun, and replace. Defaults include coverage(strict), startname(start), stopname(stop), dateformat(%tdCCYY/NN/DD), eventtype(single), eventgenerate(_failure), timeunit(days), and <frameout>_manifest for the manifest frame.

tvexpose

tvexpose using filename, id(varname) start(varname) exposure(varname) ///
    [reference(#)] entry(varname) exit(varname) [options]

Options are stop(), reference(), generate(), saveas(), frameout(), replace, merge(), evertreated, currentformer, duration(), dose, dosecuts(), continuousunit(), expandunit(), bytype, recency(), recencyunit(), grace(), lag(), washout(), pointtime, fillgaps(), carryforward(), keepvars(), check, gaps, overlaps, summarize, validate, priority(), split, layer, combine(), window(), switching, switchingdetail, statetime, keepdates, referencelabel(), label(), flow, dropinvalid, verbose, and the technical nofastpath. Categorical output is the default; dose defaults reference(0), grace(0), merge(0), lag(0), washout(0), and a derived tv_<exposure> output name; dose cannot be combined with a nonzero washout().

tvmerge

tvmerge [dataset1 dataset2 ...], id(varname) start(namelist) stop(namelist) exposure(namelist) [options]
tvmerge, frames(namelist) id(varname) start(namelist) stop(namelist) exposure(namelist) [options]

Options are frames(), generate(), prefix(), startname(), stopname(), idname(), dateformat(), saveas(), frameout(), replace, keep(), continuous(), rate(), total(), cumulative(), dropinvalid, batch(), force, check, validatecoverage, validateoverlap, summarize, flow, and verbose. continuous() is the deprecated alias for total(). The output covers only days present in every input; days covered by some inputs but not all are reported in a note and in r(n_persontime_lost).

tvevent

tvevent [using filename], id(varname) date(name) [options]
tvevent, frame(name) id(varname) date(name) [options]

Options are frame(), generate(), type(), keepvars(), continuous(), rate(), total(), cumulative(), timegen(), timeunit(), compete(), eventlabel(), startvar(), stopvar(), start(), stop(), enum(), gaptime, gapstart(), gapstop(), validate, flow, dropinvalid, verbose, and replace. Defaults include type(single), generate(_failure), timeunit(days), start(start), stop(stop), and enum(_enum) for recurring events.

tvdiagnose

tvdiagnose, id(varname) start(varname) stop(varname) [options]

Options are exposure(), entry(), exit(), coverage, gaps, overlaps, summarize, all, swimlane, maxids(), threshold(), and verbose. threshold(30) and maxids(50) are the defaults; coverage requires entry() and exit(), while summarize requires exposure().

tvweight

tvweight exposure [if] [in], covariates(varlist) [options]

Options are generate(), model(), stabilized, wtype(), truncate(), tvcovariates(), numcovariates(), id(), time(), replace, denominator(), nolog, balance, loveplot, histogram, estname(), estreplace, cumulative, cumgenerate(), ipcw(), censorcovariates(), censnumcovariates(), censgenerate(), and combgenerate(). Defaults include generate(iptw), model(logit), wtype(iptw), cumgenerate(<weight>_cum), censgenerate(ipcw), and combgenerate(<weight>_ipcw) when those modes are used.

tvage, tvband, tvsplit, and tvpanel

tvage uses id(), dob(), entry(), exit(), generate(), startgen(), stopgen(), groupwidth(), minage(), maxage(), saveas(), replace, and noisily; legacy idvar(), dobvar(), entryvar(), and exitvar() aliases remain accepted. Defaults are age_tv, age_start, age_stop, groupwidth(1), minage(0), and maxage(120).

tvband uses id(), start(), stop(), type(), origin(), width(), min(), max(), unit(), anchor(), generate(), startgen(), stopgen(), saveas(), replace, and noisily. origin() is required for age and elapsed axes and forbidden for calendar; width(1) and elapsed unit(day) are the defaults.

tvsplit uses id(), start(), stop(), age(), calendar(), elapsed(), and noisily; its nested options are width(), min(), max(), anchor(), unit(), and generate(). At least one axis is required, and the default band variables are ageband, calband, and fuband.

tvpanel uses frame(), reference(), width(), start(), stop(), period(), startgen(), stopgen(), generate(), cumulative(), prefix(), keepvars(), saveas(), replace, noisily, dropinvalid, and verbose. Defaults are reference(0), width(91), start(start), stop(stop), period(period), startgen(start), stopgen(stop), and generate(tv_class).

Key Options

Dates, identifiers, and intervals

Use numeric Stata daily dates for interval bounds and event dates; fractional dates, missing required dates, and reversed bounds are rejected unless a command explicitly offers dropinvalid. Most interval commands require the same identifier name and storage type across master and sources, reject strL identifiers, and never silently recast IDs.

The package’s intervals are closed and inclusive, so duration is stop - start + 1. When declaring the result with stset, use time0(start - 1) to map each row to Stata’s open-left convention.

Output and transactions

frameout() stages a result in a named frame and leaves the current data untouched where that option is supported; saveas() writes a file and restores the caller’s data for the commands that document that behavior. replace is required before an existing file or frame is overwritten.

tvbuild requires frameout(), leaves input frames untouched, and builds a manifest named <frameout>_manifest by default; use manifestframe() or nomanifest to control it. dryrun performs the full plan validation without committing destinations.

Quantities and exposure definitions

rate() variables remain rates when intervals are split; total() variables are apportioned by inclusive overlap days; and cumulative() variables are row-start histories carried unchanged. continuous() is a deprecated alias for total() in tvmerge and tvevent. tvsplit and tvband apply the same rules to variables carrying the [tvtools_quantity] characteristic that tvmerge, tvevent, tvexpose, and tvpanel write.

In tvexpose, categorical output uses reference() for uncovered time, while dose mode defaults the reference to zero. continuousunit() accepts days, weeks, months, quarters, or years; expandunit() defaults to that unit when continuous exposure is requested and uses fixed average widths of 7, 30.4375, 91.3125, or 365.25 days anchored at each episode start.

Stored Results

Result names below are returned in r() after successful execution; option-dependent names are returned only when their option is used.

Command Stored results
tvtools Scalar r(n_commands); macros r(commands), r(version), and r(categories)
tvspec Scalars n_sources; macros specframe, source_name for add, and source_names for list
tvbuild Scalars dryrun, spec_version, n_sources, N_persons, event_stage, dates_kept, and committed-run N_periods, n_gap_ids, uncovered_days; macros idvar, entryvar, exitvar, startvar, stopvar, source_names, payload_vars, exposure_vars, rate_vars, total_vars, cumulative_vars, specframe, frameout, coverage, manifestframe, eventvar, timevar, enumvar, gapstartvar, gapstopvar, datasignature; matrices source_counts, stage_counts
tvexpose Scalars for persons, periods, total/exposed/unexposed time, invalid-input counts, window bounds, combined states, and by-type variables; macros genvar, frameout, overlap_ids, recency_unit, recency_cutdays, combine_map, bytype_map; matrix flow when requested
tvmerge Scalars for observations, persons, datasets, quantity counts, invalid rows, gaps, overlaps, person-time lost to the input intersection (n_persontime_lost, n_ids_persontime_lost), and duplicates; macros for datasets, exposure/quantity variables, output names, and destinations; matrix flow when requested
tvevent Scalars N, N_events, quantity counts, invalid-input counts, and event validation counts; macros generate, startvar, stopvar, timegen, enum, gapstart, gapstop, and quantity variables; matrix flow when requested
tvdiagnose Scalars for each requested diagnostic, coverage/gap/overlap summaries, exposure levels, and graph status; macros id, start, stop, graph_name; matrix exposure_summary when summarize runs
tvweight Scalars for sample size, levels, ESS, weight distribution, overlap, positivity, truncation, and graph status; macros for model, weight, output, treatment/censoring, and balance settings; scalar ess_combined and matrix balance in the corresponding modes
tvage Scalars n_persons, n_persons_in, n_persons_dropped, n_observations, groupwidth; macros varname, startvar, stopvar
tvband Scalars n_persons, n_observations, width; macros axistype, varname, startvar, stopvar, total_vars
tvsplit Scalars n_axes, n_persons, n_observations; macros agevar, calvar, fuvar, startvar, stopvar, total_vars when applicable
tvpanel Scalars for persons, observations, width, and invalid-input counts; macros periodvar, startvar, stopvar, classvar, cumvars

Assumptions and Limits

  • Input dates are numeric, whole-number Stata daily dates; the suite does not interpret Stata datetime values as daily dates.
  • Identifiers are structural keys, so source names and storage types must agree and commands never pad, guess, or silently remap them.
  • tvage, tvband, and tvsplit require numeric identifiers; map string IDs deliberately before calling them.
  • tvage intentionally returns only the identifier and generated age interval variables; save or merge covariates separately.
  • tvmerge expects interval inputs and keeps only person-time covered by every input; force restricts mismatched-ID inputs to their intersection rather than inventing missing records. The dropped person-days are counted in r(n_persontime_lost).
  • tvevent treats single events as terminal and recurring event stubs as ordered stub1 through stubK members; noncanonical or noncontiguous stubs are rejected.
  • tvdiagnose counts global coverage by interval union, but category summaries can have multi-membership when exposure levels overlap.
  • tvbuild coordinates construction and provenance but does not run stset, an outcome model, an overlap-resolution choice, or a causal model.
  • Weighting output is model-based; causal interpretation requires the relevant exchangeability, positivity, treatment-model, and censoring assumptions.
  • Longitudinal stabilized weights require the analyst to retain the appropriate past-treatment and baseline history with tvweight, numcovariates() and, for censoring weights, censnumcovariates(); the outcome model must include the retained numerator terms.
  • tvweight, loveplot requires optional psdash; without it, use r(balance) to build a plot with another graphing workflow.

References

The help files cite these sources; tvevent uses the first two and tvweight the rest.

  • Andersen PK, Gill RD. Cox's regression model for counting processes: a large sample study. Annals of Statistics. 1982;10(4):1100–1120. doi:10.1214/aos/1176345976
  • Prentice RL, Williams BJ, Peterson AV. On the regression analysis of multivariate failure time data. Biometrika. 1981;68(2):373–379. doi:10.1093/biomet/68.2.373
  • Robins JM, Hernán MA, Brumback B. Marginal structural models and causal inference in epidemiology. Epidemiology. 2000;11(5):550–560. doi:10.1097/00001648-200009000-00011
  • Cole SR, Hernán MA. Constructing inverse probability weights for marginal structural models. American Journal of Epidemiology. 2008;168(6):656–664. doi:10.1093/aje/kwn164
  • Austin PC, Stuart EA. Moving towards best practice when using inverse probability of treatment weighting (IPTW) using the propensity score to estimate causal treatment effects in observational studies. Statistics in Medicine. 2015;34(28):3661–3679. doi:10.1002/sim.6607
  • Li F, Morgan KL, Zaslavsky AM. Balancing covariates via propensity score weighting. Journal of the American Statistical Association. 2018;113(521):390–400. doi:10.1080/01621459.2016.1260466
  • Li L, Greene T. A weighting analogue to pair matching in propensity score analysis. International Journal of Biostatistics. 2013;9(2):215–234. doi:10.1515/ijb-2012-0030
  • Yoshida K, Hernández-Díaz S, Solomon DH, Jackson JW, Gagne JJ, Glynn RJ, Franklin JM. Matching weights to simultaneously compare three treatment groups: comparison to three-way matching. Epidemiology. 2017;28(3):387–395. doi:10.1097/EDE.0000000000000627
  • Austin PC. Balance diagnostics for comparing the distribution of baseline covariates between treatment groups in propensity-score matched samples. Statistics in Medicine. 2009;28(25):3083–3107. doi:10.1002/sim.3697
  • Austin PC. An introduction to propensity score methods for reducing the effects of confounding in observational studies. Multivariate Behavioral Research. 2011;46(3):399–424. doi:10.1080/00273171.2011.568786

QA

QA suites and how to run them are documented in qa/README.md.

Version History

1.18.2 (2026-10-13)

  • Documentation: the README Quick Start followed by Worked Example 1 no longer fails with r(198) (frame analysis already existed); the Worked Examples setup block now returns to the default frame and drops the Quick Start frames. The .pkg description now says "twelve" public commands.

1.18.1 (2026-10-12)

  • tvevent, gaptime (and tvbuild, gaptime) now writes the gap-time clock on the package's closed-day grid: each row's gap interval is (start - origin, stop - origin + 1], the time since the preceding event (or entry), so a row ending on its origin day has gap time 1. Previously the stop was one day smaller, a row ending on its origin day had gap time 0, and the documented PWP gap-time stset declaration excluded it, losing an event on the entry day or on the day after a previous event. gapstop() values from earlier versions are one day smaller; gapstart() values are unchanged. The help recipe is now stset gapstop, time0(gapstart) failure(...) id(id_stratum), with no offset variable.
  • The default gap-time names are now _gapstart and _gapstop. The former defaults _t0 and _t collided with the variables stset creates, so the documented declaration exited with r(110).

1.18.0 (2026-10-12)

  • tvsplit and tvband now apply the suite's quantity contract. A variable tagged as an interval total ([tvtools_quantity] "total", as written by tvmerge, total() and tvevent, total()) is apportioned by inclusive days so the pieces sum to the source total; previously every piece carried the full total, so splitting one 100-unit row into three summed to 300. Rates and row-start cumulative histories are still copied unchanged, unknown quantity metadata exits with r(498), and both commands return r(total_vars).
  • tvexpose, dose with a nonzero washout() now exits with r(198). A cumulative dose already persists after its episode, so the washout tail only spread the amount over extra days and understated the dose taken by a given date (30 units over days 100–129 read as 23.33 on day 170 under washout(60)).
  • tvevent, timegen() now counts closed days from entry through each row's stop (stop - first_start + 1), matching the package's (start - 1, stop] survival-time mapping. Previously an event on the entry day had time 0 and stset dropped it. Values from earlier versions are one day (or 1/30.4375 month, 1/365.25 year) smaller.
  • tvmerge now measures person-time present in some inputs but absent from the intersected output, displays a note when it is positive, and returns r(n_persontime_lost) and r(n_ids_persontime_lost). Previously unequal per-source follow-up silently shortened the output.
  • tvexpose and tvbuild keep label() and referencelabel() text verbatim, including double quotes, backticks, and dollar signs. Previously a double quote or backtick exited with an unexplained r(198) or r(132), and text such as $name was silently replaced by the contents of a global macro. Exposure value-label text, keepvars() labels, and entry/exit labels are carried the same way, as are the master dataset label in tvbuild and tvevent event labels (eventlabel() and date-variable labels), including through tvbuild. The dose overlap path no longer prints a duplicates drop report.
  • tvweight with id() and time() accepts fractional or negative time values: the time fixed effects index the distinct values in time order (the same model) and a note says so. Previously the factor-variable expansion failed and the command reported a misleading convergence failure (r(452)). A string time() now exits with r(109).
  • Help: the tvexpose dose example now passes a total supply (rx_total) rather than a daily rate; lag() documents that it truncates episodes and removes those of lag() days or fewer; window() documents that it is anchored on the lagged start and clipped by the washout-extended stop; nofastpath is documented as a technical option; cumulative values step only at row boundaries. tvmerge documents its coverage intersection. README References now list every source the help files cite.

1.17.8 (2026-10-09)

  • Help References give DOIs in the standard doi: form.
Earlier versions

1.17.7 (2026-10-01)

  • tvexpose, priority() now assigns the highest-priority active exposure on every day and resumes a lower-priority exposure after a higher-priority one ends. Previously those resumed days were reported as the reference category at rc 0, and two overlapping same-value episodes with a different value starting between them failed with r(498). priority() now uses the same single boundary sweep as layer instead of an iterative pairwise resolver. Overlapping exposure values that priority() does not rank still exit with r(498), now with a message naming the cause. A value listed twice in priority() now exits with r(198); previously the later listing silently set its rank, so priority(2 1 2) ranked 1 above 2.
  • tvpanel computes the active class and cumulative() columns in one Mata pass per person instead of building period-by-episode pairs, a grid-by-class join, and a reshape. Output is unchanged; on 600,000 episodes (1.8 million panel rows) cumulative(years) drops from about 48 to 5 seconds and the default call from about 10 to 2.5 seconds.

1.17.6 (2026-09-30)

  • Preserve caller native S_1/S_2 bytes on tvdiagnose success/refusal, and exact filename aliases on tvmerge refusal or readonly frame output; clear unsaved memory metadata and retain actual saved-output filename/date.
  • Preserve caller native filename bytes on tvexpose refusal and frame output, clear filename metadata for new memory output, retain actual saveas() filename, and restore cold caller matastrict.
  • Preserve exact caller presence/bytes of native ReS_Call, ReS_jv2, S_1 and S_2 across recurring-event tvevent and tvbuild success or refusal. Restore native filenames on refusal; clear filename aliases for newly constructed memory output. Preserve cold interval-engine caller matastrict.

1.17.5 (2026-09-30)

  • Preserve exact caller presence/bytes of native T_gm_fix_span when tvdiagnose initializes a cold swimlane graph or refuses a call.

1.17.4 (2026-09-30)

  • Preserve caller S_1 and S_2 globals, including absence, when tvweight succeeds or refuses a call.

  • 1.17.3 (2026-09-29): tvexpose layer precedence now ranks overlapping episodes by their own start date, not the entry date they are clipped to, so an episode that began later wins after clipping and same-value merging; dose keeps only the in-window share of an episode clipped at entry or exit; washout() now reaches past entry for an episode that ended before it; an acute window() closing before entry is counted as outside follow-up instead of stopping the default path. fillgaps() now extends the episode(s) with the latest stop date instead of the latest-starting episode, whose extension could vanish inside an enclosing episode. tvevent, type(single) drops person-time after a first event that falls in an internal coverage gap (it cannot be flagged, and a later event is no longer flagged as the first) and reports it; events before the first interval remain ignored. tvband/tvsplit reject fractional widths and calendar anchors that would write fractional dates. Added release regressions with daily oracles.

  • 1.17.2 (2026-09-09): Fixed extra-exposure mapping and metadata with tvmerge, generate(), preserved caller scalars and empty-output schemas, corrected recurring-event sequence numbers across parallel strata and gap-time clocks across observation gaps, preserved quoted event labels, made failed tvspec add calls transactional, and rejected unrepresentable reference labels without committing output, and supported legal source/output names that resemble internal build variables. Added release regressions and self-contained specification examples.

  • 1.17.1 (2026-08-30): Fixed tvbuild metadata propagation for legal 32-character characteristic names, repaired Viewer-width defects in help-file synopsis tables, and completed the README inventory of stabilized numerator options.

  • 1.17.0 (2026-08-28): Expanded tvexpose's single-pass constructor across shared geometry cleaning, overlap strategies, diagnostics, and post-construction transformations; replaced quadratic dose and cumulative-panel joins with interval-sweep/running-sum engines; and reduced tvbuild peak frame residency by carrying source metadata forward before releasing source frames.

  • 1.16.0 (2026-08-13): tvage no longer discards person-time without saying so. An entry() date earlier than dob() is refused with r(498) and a row count instead of being silently truncated to the birth date, and persons dropped because their follow-up lies entirely outside minage()/maxage() are now reported unconditionally and counted in the new r(n_persons_dropped) and r(n_persons_in).

  • 1.15.0 (2026-08-10): Added explicit treatment and censoring numerator models for history-conditioned longitudinal stabilization, exposed both fitted numerator contracts in r(), corrected the IPCW documentation to match the time-specific default, and removed package-owned paging changes and decorative empty output lines.

  • 1.14.1 (2026-08-07): Corrected runtime stset guidance, verbose table headers, cumulative-weight diagnostics, graph naming, save confirmations, flow notes, event provenance, exported dataset labels, and related console-report polish; added focused regression coverage for each repaired artifact/display path.

  • 1.14.0 (2026-08-07): Renamed _dta[tvtools_pipeline*] provenance characteristics to _dta[tvtools_tvbuild*] and cleaned up all pipeline/pipe_* naming throughout documentation, demos, and QA to use tvbuild/workflow consistently.

  • 1.13.1 (2026-08-05): Corrected the closed-interval stset help conversion to use time0(start - 1) so the first follow-up day is retained.

  • 1.13.0 (2026-08-02): Harmonized console-report layout and corrected display-only leaks and formatting defects without changing estimators, interval semantics, computed values, options, or stored results.

  • 1.12.1 (2026-08-02): Hardened provenance-manifest replacement checks and improved diagnostics for damaged specification frames.

  • 1.12.0 (2026-08-01): Added tvspec, made tvbuild provenance manifests default, and made catalog rendering derive from one command list.

  • 1.11.0 (2026-07-31): Renamed the released front door from tvpipe to tvbuild and moved its specification and provenance characteristics to the new command name.

  • 1.10.2 (2026-07-31): Hardened front-door specification validation, recurring-event stub discovery, manifest return normalization, and specification-cell arity checks.

  • 1.10.1 (2026-07-30): Repaired eventlabel() forwarding in the front door and removed duplicate help content.

  • 1.10.0 (2026-07-30): Added the transactional front door for typed multi-source plans and introduced tvmerge, idname().

  • 1.9.1 (2026-07-30): Made frame-native merging preserve caller frame names and improved cumulative-weight and interval-segmentation internals without changing computed values.

  • 1.9.0 (2026-07-25): Corrected panel-mode stabilized weighting, balance-weight selection under IPCW, censoring-indicator validation, and cross-exposure overlap reporting.

  • 1.8.0 (2026-07-22): Added collision-safe output planning, explicit overlap composition codes, union-based coverage, date-first event placement, and stricter multiplicity checks.

  • 1.7.1 (2026-07-17): Improved interval-overlap performance and deterministic ordering in tvmerge.

  • 1.7.0 (2026-07-13): Consolidated interval, quantity, diagnostic, age-anniversary, and output-contract behavior across the suite.

  • 1.6.9 (2026-07-10): Added failure-safe output rollback, collision-safe labels, clipped coverage diagnostics, and early identifier validation.

  • 1.6.8 (2026-07-03): Corrected opt-in diagnostic paths, short-data error reporting, validation-file naming, and legitimate duplicate-interval handling.

  • 1.6.7 (2026-07-02): Added early strL identifier rejection and clearer numeric-ID diagnostics.

  • 1.6.6 (2026-07-02): Corrected exact-width panel endpoints, running-maximum gap logic, output-name collisions, truncation bounds, and estimation-sample IPCW validation.

  • 1.6.5 (2026-07-02): Corrected by-type labels and summaries, validation-file collisions, empty-frame output, zero-overlap validation, and non-positional continuous quantities.

  • 1.6.4 (2026-07-01): Corrected cumulative products across rows excluded from the estimation sample.

  • 1.6.3 (2026-06-30): Consolidated the shared interval engine for panel lookup and event split-point placement.

  • 1.6.2 (2026-06-29): Delegated tvweight, loveplot to optional psdash while preserving the balance table and matrix.

  • 1.6.1 (2026-06-29): Added tvband and tvsplit to the public package documentation and harmonized help presentation.

  • 1.6.0 (2026-06-29): Added IPCW and combined weights, positivity diagnostics, and recurrent-event sequence and gap-time formatting.

  • 1.5.0 (2026-06-29): Added frame-first output, harmonized aliases, and scriptable output-name returns.

  • 1.4.0 (2026-06-29): Added derived output names and collision-safe fallback naming in tvexpose.

  • 1.3.0 (2026-06-28): Added single-axis and multi-axis age, calendar, and elapsed-time splitting with exact anniversaries.

  • 1.2.0 (2026-06-28): Added Mata interval-overlap and continuous-expansion engines; deprecated batch() as an ignored compatibility option.

  • 1.1.0 (2026-06-28): Added balance diagnostics, alternative weight types, cumulative weights, graphing, frame inputs, flow reporting, and swimlanes.

  • 1.0.3 (2026-06-26): Hardened panel bookkeeping, value-label handling, and dose-overlap temporary names.

  • 1.0.2 (2026-06-19): Standardized public help-file author sections and stored-result presentation.

  • 1.0.1 (2026-06-15): Fixed suppressed parser errors, event reshaping bookkeeping, option aliases, and author metadata.

  • 1.0.0 (2026-04-08): Initial Stata-Tools release.

Author

Timothy P Copeland, Karolinska Institutet

Questions and bug reports: GitHub issues. Please include the first line of which tvtools and your Stata version.

To cite: Copeland TP. tvtools: Time-varying exposure data for survival analysis. Stata-Tools, https://github.com/tpcopeland/Stata-Tools/tree/main/tvtools. Please give the version you used (which tvtools prints it).

License

MIT (see LICENSE)