Skip to content

report.daily

Daily report generation from prediction segments.

Overview

Aggregates Segment spans, per-bucket predictions, and feature-level statistics into a DailyReport suitable for time-tracking summaries:

segments + optional per-bucket labels/features
    → core_breakdown (label → minutes)
    → mapped_breakdown (taxonomy label → minutes, optional)
    → flap rates (raw & smoothed)
    → context-switch stats
    → DailyReport

Models

ContextSwitchStats

Aggregated context-switching statistics for a day, derived from the app_switch_count_last_5m feature across all buckets.

Field Type Description
mean float Mean app switches per bucket
median float Median app switches per bucket
max_value int Peak app switches in a single bucket
total_switches int Sum of app switches across all buckets
buckets_counted int Number of buckets with valid (non-None) data

All fields have a ge=0 constraint.

DailyReport

Aggregated daily summary of predicted task-type activity.

Field Type Description
date str Calendar date (YYYY-MM-DD) this report covers
total_minutes float Total minutes of activity (sum of core_breakdown)
core_breakdown dict[str, float] Core label to total minutes mapping
mapped_breakdown dict[str, float] \| None Taxonomy label to total minutes (populated when mapped_labels are provided)
segments_count int Number of segments in the day
context_switch_stats ContextSwitchStats \| None App-switching statistics from feature data
flap_rate_raw float \| None Label changes / total windows before smoothing
flap_rate_smoothed float \| None Label changes / total windows after smoothing

total_minutes and segments_count have ge=0 constraints. The model is frozen (immutable after construction).

Functions

build_daily_report

build_daily_report(
    segments: Sequence[Segment],
    *,
    bucket_seconds: int = DEFAULT_BUCKET_SECONDS,
    raw_labels: Sequence[str] | None = None,
    smoothed_labels: Sequence[str] | None = None,
    mapped_labels: Sequence[str] | None = None,
    app_switch_counts: Sequence[float | int | None] | None = None,
) -> DailyReport

Aggregates prediction data for one calendar day into a DailyReport.

Parameter Default Description
segments (required) Prediction segments (typically from segmentize)
bucket_seconds DEFAULT_BUCKET_SECONDS (60) Width of each time bucket in seconds; scales bucket counts to minutes
raw_labels None Per-bucket labels before smoothing — used for flap_rate_raw
smoothed_labels None Per-bucket labels after smoothing — used for flap_rate_smoothed
mapped_labels None Per-bucket taxonomy-mapped labels — used for mapped_breakdown
app_switch_counts None Per-bucket app_switch_count_last_5m values — used for context_switch_stats

Raises ValueError if segments is empty.

The date field is taken from the first segment's start_ts. Flap rates are computed via flap_rate and rounded to 4 decimal places. The bucket-to-minutes conversion uses bucket_count * bucket_seconds / 60.

Usage

from taskclf.infer.smooth import segmentize, rolling_majority
from taskclf.report.daily import build_daily_report

segments = segmentize(bucket_starts, smoothed_labels, bucket_seconds=60)

report = build_daily_report(
    segments,
    raw_labels=raw_labels,
    smoothed_labels=smoothed_labels,
    mapped_labels=mapped_labels,
    app_switch_counts=app_switch_counts,
)

print(f"{report.date}: {report.total_minutes:.0f} min across {report.segments_count} segments")
for label, minutes in sorted(report.core_breakdown.items()):
    print(f"  {label}: {minutes:.1f} min")

See infer.smooth for segment and flap-rate details, and core.defaults for DEFAULT_BUCKET_SECONDS.

taskclf.report.daily

Daily report generation from prediction segments.

Aggregates segments, per-bucket predictions, and feature-level statistics into a :class:DailyReport suitable for time-tracking summaries.

ContextSwitchStats

Bases: BaseModel

Aggregated context-switching statistics for a day.

Derived from the app_switch_count_last_5m feature across all buckets in a day.

Source code in src/taskclf/report/daily.py
class ContextSwitchStats(BaseModel, frozen=True):
    """Aggregated context-switching statistics for a day.

    Derived from the ``app_switch_count_last_5m`` feature across all
    buckets in a day.
    """

    mean: float = Field(ge=0, description="Mean app switches per bucket.")
    median: float = Field(ge=0, description="Median app switches per bucket.")
    max_value: int = Field(ge=0, description="Peak app switches in a single bucket.")
    total_switches: int = Field(
        ge=0, description="Sum of app switches across all buckets."
    )
    buckets_counted: int = Field(ge=0, description="Number of buckets with valid data.")

DailyReport

Bases: BaseModel

Aggregated daily summary of predicted task-type activity.

core_breakdown maps each core label to its total minutes. mapped_breakdown does the same for user-facing taxonomy buckets (populated when per-bucket mapped labels are provided).

Source code in src/taskclf/report/daily.py
class DailyReport(BaseModel, frozen=True):
    """Aggregated daily summary of predicted task-type activity.

    ``core_breakdown`` maps each core label to its total minutes.
    ``mapped_breakdown`` does the same for user-facing taxonomy buckets
    (populated when per-bucket mapped labels are provided).
    """

    date: str = Field(description="Calendar date (YYYY-MM-DD) this report covers.")
    total_minutes: float = Field(ge=0, description="Total minutes of activity.")
    core_breakdown: dict[str, float] = Field(
        description="Core label -> total minutes mapping."
    )
    mapped_breakdown: dict[str, float] | None = Field(
        default=None, description="Mapped (taxonomy) label -> total minutes."
    )
    segments_count: int = Field(ge=0, description="Number of segments in the day.")
    context_switch_stats: ContextSwitchStats | None = Field(
        default=None,
        description="App-switching statistics from feature data.",
    )
    flap_rate_raw: float | None = Field(
        default=None,
        description="Label changes / total windows before smoothing.",
    )
    flap_rate_smoothed: float | None = Field(
        default=None,
        description="Label changes / total windows after smoothing.",
    )

build_daily_report(segments, *, bucket_seconds=DEFAULT_BUCKET_SECONDS, raw_labels=None, smoothed_labels=None, mapped_labels=None, app_switch_counts=None)

Aggregate prediction data into a :class:DailyReport.

Parameters:

Name Type Description Default
segments Sequence[Segment]

Prediction segments (typically from one calendar day).

required
bucket_seconds int

Width of each time bucket in seconds (used to convert bucket counts to minutes).

DEFAULT_BUCKET_SECONDS
raw_labels Sequence[str] | None

Per-bucket labels before smoothing — used for flap_rate_raw.

None
smoothed_labels Sequence[str] | None

Per-bucket labels after smoothing — used for flap_rate_smoothed.

None
mapped_labels Sequence[str] | None

Per-bucket taxonomy-mapped labels — used for mapped_breakdown.

None
app_switch_counts Sequence[float | int | None] | None

Per-bucket app_switch_count_last_5m values from the feature data — used for context_switch_stats.

None

Returns:

Type Description
DailyReport

A DailyReport with per-label totals, flap rates, and

DailyReport

context-switching statistics.

Raises:

Type Description
ValueError

If segments is empty.

Source code in src/taskclf/report/daily.py
def build_daily_report(
    segments: Sequence[Segment],
    *,
    bucket_seconds: int = DEFAULT_BUCKET_SECONDS,
    raw_labels: Sequence[str] | None = None,
    smoothed_labels: Sequence[str] | None = None,
    mapped_labels: Sequence[str] | None = None,
    app_switch_counts: Sequence[float | int | None] | None = None,
) -> DailyReport:
    """Aggregate prediction data into a :class:`DailyReport`.

    Args:
        segments: Prediction segments (typically from one calendar day).
        bucket_seconds: Width of each time bucket in seconds (used to
            convert bucket counts to minutes).
        raw_labels: Per-bucket labels *before* smoothing — used for
            ``flap_rate_raw``.
        smoothed_labels: Per-bucket labels *after* smoothing — used for
            ``flap_rate_smoothed``.
        mapped_labels: Per-bucket taxonomy-mapped labels — used for
            ``mapped_breakdown``.
        app_switch_counts: Per-bucket ``app_switch_count_last_5m`` values
            from the feature data — used for ``context_switch_stats``.

    Returns:
        A ``DailyReport`` with per-label totals, flap rates, and
        context-switching statistics.

    Raises:
        ValueError: If *segments* is empty.
    """
    if not segments:
        raise ValueError("Cannot build a daily report from zero segments")

    core_minutes: dict[str, float] = defaultdict(float)
    for seg in segments:
        minutes = seg.bucket_count * bucket_seconds / 60.0
        core_minutes[seg.label] += minutes

    total = sum(core_minutes.values())
    date_str = segments[0].start_ts.date().isoformat()

    mapped_breakdown: dict[str, float] | None = None
    if mapped_labels is not None:
        mb: dict[str, float] = defaultdict(float)
        bucket_minutes = bucket_seconds / 60.0
        for lbl in mapped_labels:
            mb[lbl] += bucket_minutes
        mapped_breakdown = dict(mb)

    ctx_stats = (
        _build_context_switch_stats(app_switch_counts)
        if app_switch_counts is not None
        else None
    )

    return DailyReport(
        date=date_str,
        total_minutes=round(total, 2),
        core_breakdown=dict(core_minutes),
        mapped_breakdown=mapped_breakdown,
        segments_count=len(segments),
        context_switch_stats=ctx_stats,
        flap_rate_raw=round(flap_rate(raw_labels), 4)
        if raw_labels is not None
        else None,
        flap_rate_smoothed=round(flap_rate(smoothed_labels), 4)
        if smoothed_labels is not None
        else None,
    )