Skip to content

tdfs4ds.dataset.offsets

tdfs4ds.dataset.offsets

Feature offsets (lags and leads) in datasets. Pure module (no database access).

A dataset row describes an entity at one instant t (now, an as_of date, a TimeManager's business date, or each instant of a historical window). An offset reads a feature at another instant:

  • a lag -L reads the feature at t - L (autoregressive inputs: amount yesterday, the day before, last month);
  • a lead +L reads it at t + L (forecast targets: churn one month later).

Equivalently, a value valid over [s, e) is, as a lag of L, valid over [s + L, e + L): the period is shifted, then the usual temporal logic applies.

Two rules shape the SQL (verified on Vantage 20 by reading EXPLAIN plans):

  • the shift is always applied to the CONSTANT side of a comparison (ValidStart <= t - L), never to the column (ValidStart + L <= t); Teradata folds the constant arithmetic into a literal, and each storage table is still read "from a single partition";
  • a lead never reads past the database's current time: t + L beyond now is not an observation, so the value is NULL there (an open stored period, "until changed", is not a value of the future).

Offsets are whole seconds or calendar months: valid periods are stored as TIMESTAMP(0) WITH TIME ZONE whatever the source's own timestamp type, so a sub-second offset could not change any result.

MAX_DAYS module-attribute

MAX_DAYS = 9999

LAG module-attribute

LAG = 'LAG'

LEAD module-attribute

LEAD = 'LEAD'

PIVOT module-attribute

PIVOT = 'PIVOT'

PER_OFFSET module-attribute

PER_OFFSET = 'PER_OFFSET'

OFFSET_STRATEGIES module-attribute

OFFSET_STRATEGIES = (PIVOT, PER_OFFSET)

_HINT module-attribute

_HINT = "Write an offset with its sign: '-1 day' (the value one day BEFORE the dataset's instant, a lag) or '+1 month' (one month AFTER, a lead, e.g. a forecast target); '0' is the instant itself. Units: year, month, week, day, hour, minute, second (ISO 8601 also works: '-P1D', '+P1M', '-PT6H')."

_SUBSECOND module-attribute

_SUBSECOND = "offsets are whole seconds: tdfs4ds stores every valid period as TIMESTAMP(0) WITH TIME ZONE (whatever the source's timestamp type), so a fraction of a second cannot change a result. Milliseconds and microseconds are not accepted."

_SIGN_RE module-attribute

_SIGN_RE = compile('^\\s*([+-])\\s*(.*)$', DOTALL)

_SUBSECOND_RE module-attribute

_SUBSECOND_RE = compile('\\b(ms|msec|millisecond|milliseconds|us|usec|microsecond|microseconds)\\b', I)

_ADD_MONTHS_RE module-attribute

_ADD_MONTHS_RE = compile('^ADD_MONTHS\\s*\\((?P<body>.*)\\)$', IGNORECASE | DOTALL)

_INTERVAL_RE module-attribute

_INTERVAL_RE = compile("^\\((?P<expr>.*)\\s(?P<sign>[+-])\\s+INTERVAL\\s*'(?P<d>\\d+)\\s+(?P<h>\\d{2}):(?P<mi>\\d{2}):(?P<s>\\d{2})'\\s+DAY\\s+TO\\s+SECOND\\s*\\)$", IGNORECASE | DOTALL)

__all__ module-attribute

__all__ = ['Offset', 'parse_offset', 'resolve_offsets', 'offset_alias', 'split_shift', 'offset_from', 'LAG', 'LEAD', 'PIVOT', 'PER_OFFSET', 'OFFSET_STRATEGIES', 'MAX_DAYS']

_FR_UNITS module-attribute

_FR_UNITS = {'year': 'an', 'month': 'mois', 'day': 'jour', 'hour': 'heure', 'minute': 'minute', 'second': 'seconde'}

Duration

A non-negative length of time: a calendar part (months) and an exact part (seconds).

The two are kept apart because a month has no fixed length: months are applied with ADD_MONTHS (which clamps month-ends — 31 May minus three months is 29 Feb, verified live; - INTERVAL 'n' MONTH would raise Error 2665 instead), the rest with a DAY TO SECOND interval.

__slots__ class-attribute instance-attribute

__slots__ = ('months', 'seconds')

months instance-attribute

months = int(months)

seconds instance-attribute

seconds = int(seconds)

__init__

__init__(months=0, seconds=0)

is_zero

is_zero()

__eq__

__eq__(other)

__hash__

__hash__()

__repr__

__repr__()

iso

iso()

ISO 8601 form (P1Y2M3DT4H5M6S); PT0S for zero.

text

text()

Readable form: 1 YEAR 2 MONTH 3 DAY 04:05:06.

interval_literal

interval_literal()

INTERVAL 'd hh:mi:ss' DAY TO SECOND for the exact part.

No precision on DAY: DAY(4) TO SECOND inside a literal is a syntax error (Error 3706, verified live); the precision is taken from the literal itself, and 9999 days is accepted.

apply

apply(expr, sign)

SQL for expr ± self; sign is +1 or -1.

Months first (innermost), then the exact part — "back one month, then back two days". The order is fixed so the text round-trips.

Offset

A signed offset: sign (-1 lag, +1 lead) and a non-zero Duration.

Offset.zero() (duration zero) is the dataset's own instant.

__slots__ class-attribute instance-attribute

__slots__ = ('sign', 'duration')

duration instance-attribute

duration = duration if duration is not None else Duration()

sign instance-attribute

sign = sign if not is_zero() else 1

is_zero property

is_zero

direction property

direction

is_lead property

is_lead

months property

months

Signed months.

seconds property

seconds

Signed seconds.

__init__

__init__(sign, duration)

zero classmethod

zero()

__eq__

__eq__(other)

__hash__

__hash__()

__repr__

__repr__()

negated

negated()

iso

iso()

Signed ISO 8601: -P1D, +P1M, PT0S.

text

text()

Readable: 1 day before, 1 month after, at the instant.

suffix

suffix()

Column-name suffix: LAG_1D, LEAD_1MO, LAG_1D12H; '' for zero.

apply

apply(expr)

SQL for expr moved by this offset (expr - L for a lag, + L for a lead).

catalog_row

catalog_row()

parse_duration

parse_duration(value, name='duration')

Read value into a Duration. None stays None.

Accepted: '3 months', '90 days', '1 year 2 months', '2 days 6 hours', ISO 8601 ('P1Y2M', 'PT12H'), (3, 'months'), a datetime.timedelta (whole seconds) or a Duration.

_human

_human(d)

parse_offset

parse_offset(value)

Read value into an Offset.

Accepted: '0' / 0 (the instant), '-1 day', '+1 month', '-1 month 2 days', signed ISO 8601 ('-P1D', '+PT6H'), a signed (n, unit) tuple, a datetime.timedelta (its sign) or an Offset. A non-zero value without a sign is refused: lag or lead must be explicit.

resolve_offsets

resolve_offsets(selected_features, feature_offsets)

{FEATURE_NAME_UPPER: [Offset, ...]} from the user's feature_offsets.

Every key must be a selected feature (case-insensitive). A single value is accepted for a list of one; duplicates are dropped, the order kept. None or {} gives {} (no offset at all: the historical output).

offset_alias

offset_alias(base, offset)

The column name of base read at offset (base itself at zero).

split_shift

split_shift(expr)

(base_text, months, seconds): expr = base moved by that signed amount.

Reads the shapes Offset.apply writes (ADD_MONTHS(x, n) innermost, then (x ± INTERVAL 'd hh:mi:ss' DAY TO SECOND)); anything else is its own base with no shift.

offset_from

offset_from(months, seconds)

The Offset of a signed (months, seconds) shift; refused when the signs disagree.

base_alias

base_alias(alias, offset)

The column name alias had before its offset suffix (itself at zero / None).

offset_columns

offset_columns(spec, base_names=None)

The columns of spec that read a feature at an offset.

One dict per column: alias, base (the feature's catalog name when base_names maps FEATURE_ID to it, else the alias without its suffix), feature_id, version, table, offset.

_human_fr

_human_fr(d)

renamed_columns

renamed_columns(spec, base_names=None, views=None)

Columns whose name is not their feature's catalog name: offsets and aliases.

Like :func:offset_columns, plus the columns without offset whose name differs from the feature's name in base_names ({FEATURE_ID: name}; a feature whose name is unknown is left out). views {PROCESS_ID: view} adds view, the process computing that version.

describe_offset_column

describe_offset_column(base, offset, base_description=None, french=False, view=None)

Fixed sentence documenting an offset or aliased column (no LLM).

offset None: a column named by an alias, read at the dataset's instant; view names the process computing that version.