"""Harbor Helpdesk, Module 02: read-only standard-library worked example. Usage: python3 worked_analysis.py tickets.csv Fixed teaching scope/cutoff, no network/storage writes. Not a production pipeline. """ import csv import datetime as dt import json import statistics import sys UTC = dt.timezone.utc SNAPSHOT = dt.datetime(2026, 6, 22, tzinfo=UTC) START = dt.datetime(2026, 6, 1, tzinfo=UTC) SPLIT = dt.datetime(2026, 6, 8, tzinfo=UTC) END = dt.datetime(2026, 6, 15, tzinfo=UTC) BIN_LABELS = ['[0,1)', '[1,2)', '[2,4)', '[4,8)', '[8,infinity)'] REQUIRED = ['ticket_id', 'created_at_utc', 'channel', 'queue', 'priority', 'first_response_at_utc', 'first_resolved_at_utc', 'status_at_snapshot', 'satisfaction_1_to5'] def timestamp(value): result = dt.datetime.fromisoformat(value.replace('Z', '+00:00')) if result.tzinfo is None or result.utcoffset() != dt.timedelta(0): raise ValueError('Timestamps must explicitly use UTC.') return result def distribution(values): ordered = sorted(values) n = len(ordered) return {'observed': n, 'sum_hours': sum(ordered), 'mean_hours': statistics.mean(ordered) if n else None, 'median_hours': statistics.median(ordered) if n else None, 'minimum_hours': min(ordered) if n else None, 'maximum_hours': max(ordered) if n else None, 'sorted_hours': ordered, 'bin_counts': [sum(0 <= v < 1 for v in ordered), sum(1 <= v < 2 for v in ordered), sum(2 <= v < 4 for v in ordered), sum(4 <= v < 8 for v in ordered), sum(v >= 8 for v in ordered)]} def summarize(rows): durations = [r['response_hours'] for r in rows if r['response_hours'] is not None] ratings = [r['rating'] for r in rows if r['rating'] is not None] mature = [r for r in rows if r['mature_72h']] timely = sum(v <= 4 for v in durations) resolved = sum(r['resolution_hours'] is not None and r['resolution_hours'] <= 72 for r in mature) return {'eligible': len(rows), 'observed_responses': len(durations), 'timely': timely, 'timely_rate': timely / len(durations) if durations else None, 'response_coverage': len(durations) / len(rows) if rows else None, 'mature_for_72h': len(mature), 'resolved_within_72h': resolved, 'resolution_rate': resolved / len(mature) if mature else None, 'rating_observed': len(ratings), 'rating_mean': statistics.mean(ratings) if ratings else None, 'rating_coverage': len(ratings) / len(rows) if rows else None, 'response_distribution': distribution(durations)} def analyze(path): parsed = [] seen = set() with open(path, newline='', encoding='utf-8-sig') as handle: reader = csv.DictReader(handle) if reader.fieldnames != REQUIRED: raise ValueError('Unexpected columns: use the documented source schema.') for source in reader: key = source['ticket_id'] if not key or key in seen: raise ValueError('Missing or duplicate ticket key; investigate, do not drop silently.') seen.add(key) if source['channel'] not in ['email', 'chat'] or source['queue'] not in ['general', 'billing'] or source['priority'] not in ['high', 'normal'] or source['status_at_snapshot'] not in ['resolved', 'open']: raise ValueError('Unsupported category.') created = timestamp(source['created_at_utc']) if not START <= created < END: raise ValueError('Out-of-scope creation timestamp; review exclusions explicitly.') events = [timestamp(source[f]) if source[f] else None for f in ['first_response_at_utc', 'first_resolved_at_utc']] for event in events: if event is not None and not created <= event <= SNAPSHOT: raise ValueError('Invalid event chronology or snapshot coverage.') if events[0] is not None and events[1] is not None and events[1] < events[0]: raise ValueError('Resolution precedes response; investigate the record.') if (source['status_at_snapshot'] == 'resolved') != (events[1] is not None): raise ValueError('Status and resolution conflict under this simplified case dictionary.') rating = int(source['satisfaction_1_to5']) if source['satisfaction_1_to5'] else None if rating is not None and not 1 <= rating <= 5: raise ValueError('Rating outside 1–5.') parsed.append({'ticket_id': key, 'cohort': 'week_1' if created < SPLIT else 'week_2', 'channel': source['channel'], 'priority': source['priority'], 'response_hours': (events[0] - created).total_seconds()/3600 if events[0] is not None else None, 'resolution_hours': (events[1] - created).total_seconds()/3600 if events[1] is not None else None, 'mature_72h': SNAPSHOT-created >= dt.timedelta(hours=72), 'rating': rating}) cohorts = [] for name in ['week_1', 'week_2']: group = [r for r in parsed if r['cohort'] == name] result = summarize(group) result['cohort'] = name result['channels'] = {c: summarize([r for r in group if r['channel'] == c]) for c in ['email', 'chat']} result['priorities'] = {p: summarize([r for r in group if r['priority'] == p]) for p in ['high', 'normal']} cohorts.append(result) def weighted(result, weights): if any(weights[c] is None or (weights[c] > 0 and result['channels'][c]['timely_rate'] is None) for c in weights): return None return sum(weights[c] * result['channels'][c]['timely_rate'] for c in weights if weights[c] > 0) reference = cohorts[0] w1_weights = {c: reference['channels'][c]['eligible']/reference['eligible'] if reference['eligible'] else None for c in ['email', 'chat']} # Weights are ticket shares; when response coverage differs, investigate before interpreting standardization. return {'case': 'Harbor Helpdesk — fictional teaching data', 'version': '2026-10-15.2', 'snapshot_utc': SNAPSHOT.isoformat(), 'source_count': len(parsed), 'bin_labels_hours': BIN_LABELS, 'cohorts': cohorts, 'combined': summarize(parsed), 'combined_channels': {c: summarize([r for r in parsed if r['channel'] == c]) for c in ['email', 'chat']}, 'fixed_week1_ticket_weights': w1_weights, 'fixed_week1_weighted_rates': [weighted(r, w1_weights) for r in cohorts], 'equal_channel_weighted_rates': [weighted(r, {'email': .5, 'chat': .5}) for r in cohorts], 'derived_tickets': parsed, 'interpretation': 'Descriptive summaries, not causal effects. Missing observations stay unavailable. Fixed ticket weights require coverage review if coverage differs.'} if __name__ == '__main__': if len(sys.argv) != 2: raise SystemExit('Usage: python3 worked_analysis.py tickets.csv') print(json.dumps(analyze(sys.argv[1]), indent=2, ensure_ascii=False, allow_nan=False))