File size: 43,055 Bytes
367f592
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
-- =============================================================================
-- frontal-lobe2 : player usage & situational-confidence pond (Phase 1)
-- =============================================================================
-- Built the way the user built it: identify TWO stats first -- the spine -- then
-- layer every other stat on top of that spine. The layers are only meaningful
-- because the spine decides who we are even asking about.
--
--   LAYER 1  THE SPINE -- position-gated opportunity. "Who does he go to?"
--              if WR or TE  -> TARGETS
--              if RB or FB  -> RUSH ATTEMPTS
--            Nothing else. A WR's confidence is measured in targets and a
--            back's in carries, because those are the touches the coach
--            actually assigns. Mixing them into one "touches" number lets a
--            checkdown back outrank a WR1 and tells you nothing.
--
--   LAYER 2  STRESS -- the user's core insight, quoted:
--              "the higher the stress level the more they go to who they know"
--            So the signal is not how big a share he gets. It is whether his
--            share GROWS as the situation tightens. A coach spreading the ball
--            around on 1st-and-10 in the 2nd quarter is calling an offense. A
--            coach going to the same man on 3rd-and-4 at the 8 with a
--            three-point lead in the 4th is telling you who he trusts.
--            Measured as: share under high stress MINUS share under low
--            stress. That delta is the indicator.
--
--   LAYER 3  THE DEPTH CHART -- the coach's own published hierarchy, compared
--            to what the ball actually says. Out-earning your listed slot is
--            the most perishable signal here, because the chart is public and
--            slow while usage is real and fast.
--
--   LAYER 4  WHERE -- field zone (Inside 20/10/5), pass location, air yards.
--            "in the red zone, where were the passes thrown."
--
--   LAYER 5  RETURN -- Yards From Scrimmage / yards per touch. PFR's
--            /years/2026/scrimmage.htm. "productive players once they touch
--            the ball." Reused from silver.nfl_player_scrimmage_week so a
--            touch means one thing in this lake.
--
-- EVERY SHARE IS INTRA-TEAM. Per the user: the measurement is confidence
-- "within his own team", not across the NFL. A WR on a pass-heavy offense
-- out-targets a trusted WR1 on a run-first team without either fact saying
-- anything about the two coaches. So every denominator below is that player's
-- OWN TEAM's total in that same game.
--
-- SOURCES -- nflverse only (CC-BY-4.0, nightly). No new vendor needed:
--   pbp           play_by_play_2026.csv.gz  -> every target and every carry,
--                 with down, distance, clock, score, field position. Same file
--                 ingest_nfl_td_logs.py already pulls.
--   depth_charts  depth_charts_2026.csv     -> the stated hierarchy,
--                 point-in-time (197 snapshots Mar->Sep 2026, verified).
--
-- PFR is never fetched (403 Cloudflare + ToS). docs/PFR_SOURCE_NOTE.md.
-- ffverse/ffopportunity expected-points is verified and real but deliberately
-- NOT used in Phase 1: it is GPL-3.0, weekly rather than nightly, and a model
-- output rather than an observed fact. (game_id, play_id) is preserved below
-- so it can be joined 1:1 as a Phase 2 enrichment.
--
-- Engine: DuckDB. Depends on sql/nfl_pfr_schema.sql and
-- sql/nfl_schedule_schema.sql.
-- =============================================================================

CREATE SCHEMA IF NOT EXISTS silver;
CREATE SCHEMA IF NOT EXISTS gold;

-- =============================================================================
-- BASE TABLE: one row per OPPORTUNITY -- a target or a carry.
--
-- GRAIN: (game_id, play_id, opportunity_type). Same event-grain philosophy as
-- silver.nfl_td_log: store the decision once, express every chart as a view.
-- A single play can be only one of the two, so in practice this is one row per
-- play, but the composite key keeps it honest.
--
-- An "opportunity" is a coaching decision to put the ball in a named player's
-- hands. Sacks, scrambles, throwaways and spikes have no intended recipient in
-- the data and are excluded -- they are not decisions to trust anyone.
-- =============================================================================
CREATE TABLE IF NOT EXISTS silver.nfl_opportunity_event (
    season                  INTEGER NOT NULL,
    week                    INTEGER NOT NULL,
    season_type             VARCHAR,
    game_id                 VARCHAR NOT NULL,
    play_id                 INTEGER NOT NULL,
    opportunity_type        VARCHAR NOT NULL,      -- 'target' | 'rush'
    -- who
    posteam                 VARCHAR NOT NULL,      -- the offense; whose coach we are reading
    defteam                 VARCHAR,
    player_id               VARCHAR,               -- receiver on a target, rusher on a carry
    player_name             VARCHAR,
    passer_player_id        VARCHAR,               -- NULL on a rush
    passer_player_name      VARCHAR,

    -- ---- LAYER 4: WHERE ------------------------------------------------
    yardline_100            INTEGER,               -- yards from the opponent goal line
    is_inside_20            BOOLEAN,               -- PFR "Inside 20"
    is_inside_10            BOOLEAN,               -- PFR "Inside 10"
    is_inside_5             BOOLEAN,               -- PFR "Inside 5"
    is_goal_to_go           BOOLEAN,
    air_yards               INTEGER,               -- targets only
    pass_location           VARCHAR,               -- left | middle | right (targets)
    run_location            VARCHAR,               -- left | middle | right (rushes)
    run_gap                 VARCHAR,               -- guard | tackle | end (rushes)

    -- ---- LAYER 2: STRESS ----------------------------------------------
    -- The situation as the play was called. All four inputs come straight
    -- from pbp; none are inferred.
    down                    INTEGER,
    ydstogo                 INTEGER,
    qtr                     INTEGER,
    game_seconds_remaining  INTEGER,
    score_differential      INTEGER,               -- offense minus defense, pre-play
    -- The stress ladder. Documented explicitly so it can be argued with
    -- rather than reverse-engineered:
    --   3 MAX      -- goal to go, inside the 5, 4th down, or a one-score
    --                 game in the last 5 minutes. No margin for error.
    --   2 HIGH     -- red zone, 3rd down, or 2 yards or fewer to go.
    --                 Conversion or scoring pressure.
    --   1 MODERATE -- 2nd and long, or trailing by 9-16 (two-score chase).
    --   0 NEUTRAL  -- everything else. Open field, script territory.
    stress_level            INTEGER NOT NULL,
    is_high_stress          BOOLEAN NOT NULL,      -- stress_level >= 2
    is_two_minute           BOOLEAN,               -- last 2 min of either half
    -- QB run type. A designed keeper or sneak is a CALLED PLAY -- the coach
    -- chose the quarterback over his back, which is a confidence statement and
    -- belongs in the pool. A scramble is a collapsed pocket: nobody decided to
    -- give him the ball. Flagged, not deleted, so either reading is available.
    is_qb_scramble          BOOLEAN,
    is_designed_run         BOOLEAN,               -- rush that was not a scramble

    -- ---- outcome (Layer 5 inputs) --------------------------------------
    completed               BOOLEAN,               -- complete_pass; NULL-ish on rush (true)
    yards_gained            INTEGER,
    yards_after_catch       INTEGER,
    touchdown               BOOLEAN,
    first_down              BOOLEAN,
    turnover                BOOLEAN,               -- interception on a target
    play_description        VARCHAR,
    ingest_run_id           VARCHAR NOT NULL,
    PRIMARY KEY (season, game_id, play_id, opportunity_type)
);

CREATE INDEX IF NOT EXISTS ix_opp_player  ON silver.nfl_opportunity_event (season, player_id, week);
CREATE INDEX IF NOT EXISTS ix_opp_team    ON silver.nfl_opportunity_event (season, posteam, week);
CREATE INDEX IF NOT EXISTS ix_opp_stress  ON silver.nfl_opportunity_event (season, posteam, is_high_stress);
CREATE INDEX IF NOT EXISTS ix_opp_type    ON silver.nfl_opportunity_event (season, posteam, opportunity_type);

-- =============================================================================
-- BASE TABLE: depth chart snapshots (Layer 3) -- the coach's stated hierarchy.
--
-- This source is a POINT-IN-TIME snapshot series, not a weekly table. A depth
-- chart read today says nothing about who the WR2 was in week 1, so every
-- join below goes through v_nfl_depth_chart_pregame, which takes the latest
-- snapshot at or before that game's kickoff. Same point-in-time discipline the
-- props table enforces with is_pregame -- and for the same reason: otherwise a
-- mid-week promotion leaks backwards into a metric describing a past decision.
--
-- snapshot_at is TIMESTAMPTZ deliberately. The source publishes ISO-8601 with
-- a literal Z; storing that naive is exactly the bug already found and fixed
-- on gold.nfl_prop_line_rotowire, where local time sat in a column named _utc.
-- =============================================================================
CREATE TABLE IF NOT EXISTS silver.nfl_depth_chart_snapshot (
    snapshot_at             TIMESTAMPTZ NOT NULL,
    team                    VARCHAR NOT NULL,
    pos_grp                 VARCHAR,               -- '3WR 1TE' = offense
    pos_abb                 VARCHAR NOT NULL,      -- QB | RB | WR | TE | FB | OL...
    pos_name                VARCHAR,
    pos_slot                INTEGER,
    pos_rank                INTEGER NOT NULL,      -- 1 = starter at that spot
    gsis_id                 VARCHAR,               -- joins silver.nfl_player.player_id
    espn_id                 VARCHAR,
    player_name             VARCHAR NOT NULL,
    ingest_run_id           VARCHAR NOT NULL,
    PRIMARY KEY (snapshot_at, team, pos_abb, pos_rank, player_name)
);
CREATE INDEX IF NOT EXISTS ix_depth_team_at ON silver.nfl_depth_chart_snapshot (team, snapshot_at);
CREATE INDEX IF NOT EXISTS ix_depth_gsis    ON silver.nfl_depth_chart_snapshot (gsis_id, snapshot_at);

-- =============================================================================
-- LAYER 1 -- THE SPINE
-- =============================================================================
-- Team opportunity totals: the denominator for every share in this pond. Split
-- by type, because the spine is position-gated -- a WR's share is of the
-- team's TARGETS, a back's is of the team's CARRIES. One definition, one place.
-- QB DESIGNED RUNS ARE IN THE RUSH POOL. Per the user, and he is right:
--
--   "in that situation the team is inside of the 5 yardline therefore qb if he
--    runs is counted as a running back in my system, because the actual rb is
--    competing with the qb in an area where he should get the ball and that too
--    is a measurement of the coaches confidence"
--
-- An earlier version of this view excluded QB carries, on the theory that the
-- goal-line sneak was a structural artifact crushing every back's stress share.
-- That was wrong, and it deleted the signal instead of measuring it. If a coach
-- is inside the 5 -- the one place a starting back should get the ball -- and
-- calls a keeper instead, that IS the confidence statement. A back's share
-- falling under stress because his quarterback takes the sneak is the finding,
-- not a bug obscuring one.
--
-- The one distinction kept: a DESIGNED run (sneak, keeper, draw) is a called
-- play, so it counts. A SCRAMBLE is a collapsed pocket -- nobody chose to give
-- him the ball -- so it does not. Both stay in the event table, flagged, so
-- this choice is auditable rather than baked in.
--
-- The user's own cross-check on whether this unfairly buries starters:
--   "it will not effect the starting RB overall because if you look at the
--    rushing inside the 20 and success rate then more than likely it is going
--    to the starting rb"
-- That check is v_nfl_redzone_tiers (inside-20 opportunities and TDs), which is
-- exactly why the tiers live alongside the stress split rather than inside it.
CREATE OR REPLACE VIEW silver.v_nfl_team_opportunity_volume AS
WITH e AS (
    SELECT o.*, COALESCE(p.position, 'UNK') AS position
    FROM silver.nfl_opportunity_event o
    LEFT JOIN silver.nfl_player p ON p.player_id = o.player_id
    -- scrambles are not a coaching decision; everything else counts
    WHERE o.opportunity_type = 'target' OR COALESCE(o.is_designed_run, true)
)
SELECT season, week, game_id, posteam AS team,
       count(*) FILTER (WHERE opportunity_type = 'target')        AS team_targets,
       count(*) FILTER (WHERE opportunity_type = 'rush')          AS team_rushes,
       count(*) FILTER (WHERE opportunity_type = 'target' AND is_high_stress)
                                                                   AS team_targets_stress,
       count(*) FILTER (WHERE opportunity_type = 'rush' AND is_high_stress)
                                                                   AS team_rushes_stress,
       count(*) FILTER (WHERE opportunity_type = 'target' AND NOT is_high_stress)
                                                                   AS team_targets_calm,
       count(*) FILTER (WHERE opportunity_type = 'rush' AND NOT is_high_stress)
                                                                   AS team_rushes_calm,
       count(*) FILTER (WHERE is_inside_20)                        AS team_opps_inside_20,
       count(*) FILTER (WHERE is_inside_10)                        AS team_opps_inside_10,
       count(*) FILTER (WHERE is_inside_5)                         AS team_opps_inside_5,
       count(*) FILTER (WHERE is_goal_to_go)                       AS team_opps_g2g,
       count(*) FILTER (WHERE stress_level = 3)                    AS team_opps_max_stress,
       -- the QB's slice of the pool, surfaced rather than hidden: this is the
       -- number that explains a back's stress share, so it should be readable
       -- next to it instead of buried in a denominator
       count(*) FILTER (WHERE opportunity_type = 'rush' AND position = 'QB')
                                                                   AS team_qb_designed_runs,
       count(*) FILTER (WHERE opportunity_type = 'rush' AND position = 'QB'
                          AND is_inside_5)                         AS team_qb_runs_inside_5,
       count(*) FILTER (WHERE opportunity_type = 'rush' AND position = 'QB'
                          AND is_high_stress)                      AS team_qb_runs_stress
FROM e
GROUP BY season, week, game_id, posteam;

-- THE SPINE ITSELF. One row per player per game, carrying only the
-- position-appropriate opportunity metric and its intra-team share.
--
-- spine_metric names which stat is being counted so nothing downstream has to
-- guess, and so a mis-gated position is visible rather than silent.
CREATE OR REPLACE VIEW silver.v_nfl_opportunity_spine AS
WITH counted AS (
    SELECT e.season, e.week, e.game_id, e.posteam AS team, e.player_id,
           ANY_VALUE(e.player_name)                                  AS player_name,
           count(*) FILTER (WHERE e.opportunity_type = 'target')      AS targets,
           count(*) FILTER (WHERE e.opportunity_type = 'rush')        AS rushes,
           count(*) FILTER (WHERE e.opportunity_type = 'target' AND e.is_high_stress)
                                                                       AS targets_stress,
           count(*) FILTER (WHERE e.opportunity_type = 'rush'   AND e.is_high_stress)
                                                                       AS rushes_stress,
           count(*) FILTER (WHERE e.opportunity_type = 'target' AND NOT e.is_high_stress)
                                                                       AS targets_calm,
           count(*) FILTER (WHERE e.opportunity_type = 'rush'   AND NOT e.is_high_stress)
                                                                       AS rushes_calm
    FROM silver.nfl_opportunity_event e
    WHERE e.player_id IS NOT NULL
      -- scrambles excluded from a player's own count too, matching the
      -- denominator in v_nfl_team_opportunity_volume
      AND (e.opportunity_type = 'target' OR COALESCE(e.is_designed_run, true))
    GROUP BY e.season, e.week, e.game_id, e.posteam, e.player_id
)
-- THE GATE
--   WR, TE  -> targets
--   RB, FB  -> rush attempts
--   QB      -> rush attempts, per the user: "qb if he runs is counted as a
--              running back in my system". Labeled rush_attempts_qb so a
--              quarterback appearing high on a rushing leaderboard is obviously
--              a quarterback and not a mislabel -- when Hurts tops Philadelphia
--              inside the 5, that is the actual finding, not an error.
--
-- No inference. An earlier version fell back to "whichever stat he accumulated
-- more of" for unlabeled positions, which silently pulled QBs in under a name
-- that looked like a real gate. Guessing the gate defeats having one.
SELECT c.season, c.week, c.game_id, c.team, c.player_id, c.player_name,
       p.position,
       CASE WHEN p.position = 'QB'          THEN 'rush_attempts_qb'
            WHEN p.position IN ('RB', 'FB') THEN 'rush_attempts'
            ELSE 'targets' END                                  AS spine_metric,
       CASE WHEN p.position IN ('RB', 'FB', 'QB') THEN c.rushes
            ELSE c.targets END                                  AS spine_opportunities,
       CASE WHEN p.position IN ('RB', 'FB', 'QB') THEN c.rushes_stress
            ELSE c.targets_stress END                           AS spine_opps_stress,
       CASE WHEN p.position IN ('RB', 'FB', 'QB') THEN c.rushes_calm
            ELSE c.targets_calm END                             AS spine_opps_calm,
       -- the matching team denominator for that same metric
       CASE WHEN p.position IN ('RB', 'FB', 'QB') THEN v.team_rushes
            ELSE v.team_targets END                             AS team_spine_opportunities,
       CASE WHEN p.position IN ('RB', 'FB', 'QB') THEN v.team_rushes_stress
            ELSE v.team_targets_stress END                       AS team_spine_opps_stress,
       CASE WHEN p.position IN ('RB', 'FB', 'QB') THEN v.team_rushes_calm
            ELSE v.team_targets_calm END                         AS team_spine_opps_calm,
       -- both raw counts kept: a receiving back's targets still matter, they
       -- are just not his spine
       c.targets, c.rushes,
       -- the QB's share of this team's rush pool, carried on every row. This is
       -- the number that EXPLAINS a back's stress share, so it should be
       -- readable beside it rather than buried in a denominator.
       v.team_qb_designed_runs, v.team_qb_runs_inside_5, v.team_qb_runs_stress
FROM counted c
LEFT JOIN silver.nfl_player p ON p.player_id = c.player_id
JOIN silver.v_nfl_team_opportunity_volume v
  ON v.game_id = c.game_id AND v.team = c.team
WHERE p.position IN ('WR', 'TE', 'RB', 'FB', 'QB');

-- =============================================================================
-- LAYER 2 -- STRESS. The heart of the indicator.
--
-- "the higher the stress level the more they go to who they know"
--
-- confidence_delta = his share of the team's spine opportunities when the
-- situation is tight, MINUS his share when it is not. Positive means the coach
-- concentrates on him under pressure. That is the measurement.
--
-- Read alongside spine_opps_stress: a delta computed off 2 high-stress
-- opportunities is noise, which is why the raw counts travel with it and why
-- the indicator applies shrinkage rather than trusting the ratio directly.
-- =============================================================================
CREATE OR REPLACE VIEW silver.v_nfl_stress_split AS
SELECT season, team, player_id,
       ANY_VALUE(player_name)                                    AS player_name,
       ANY_VALUE(spine_metric)                                   AS spine_metric,
       count(DISTINCT game_id)                                    AS games,
       sum(spine_opportunities)                                   AS spine_opps,
       sum(team_spine_opportunities)                              AS team_spine_opps,
       sum(spine_opps_stress)                                     AS spine_opps_stress,
       sum(team_spine_opps_stress)                                AS team_spine_opps_stress,
       sum(spine_opps_calm)                                       AS spine_opps_calm,
       sum(team_spine_opps_calm)                                  AS team_spine_opps_calm,
       -- overall intra-team share of his own position's opportunity pool
       round(1.0 * sum(spine_opportunities)
             / NULLIF(sum(team_spine_opportunities), 0), 4)        AS share_overall,
       -- share when it is tight, and when it is not
       round(1.0 * sum(spine_opps_stress)
             / NULLIF(sum(team_spine_opps_stress), 0), 4)          AS share_stress,
       round(1.0 * sum(spine_opps_calm)
             / NULLIF(sum(team_spine_opps_calm), 0), 4)            AS share_calm,
       -- THE SIGNAL
       round(1.0 * sum(spine_opps_stress) / NULLIF(sum(team_spine_opps_stress), 0)
           - 1.0 * sum(spine_opps_calm)   / NULLIF(sum(team_spine_opps_calm), 0), 4)
                                                                   AS confidence_delta
FROM silver.v_nfl_opportunity_spine
GROUP BY season, team, player_id;

-- =============================================================================
-- LAYER 4 -- WHERE. Field-zone tiers, shaped like PFR's red-zone pages
-- (/years/2026/redzone-passing.htm, redzone-rushing.htm): three nested tiers,
-- each with opportunities, yards and TDs.
--
-- The "%" column on those PFR pages is a TEAM share, which is also the only
-- version that answers this pond's question -- a league-wide red-zone
-- percentage just ranks offenses. Named _intra so that is unambiguous.
--
-- _shrunk columns exist because red-zone samples are genuinely tiny. Confirmed
-- on real weeks 1-2 data: teams had between 0 and 18 inside-20 opportunities
-- for the entire season so far. A raw share on a denominator of 5 is mostly
-- noise -- 2-of-5 reads as 40% and would outrank a far more clearly trusted
-- player at 6-of-18. Adding k=4 to the denominator pulls small samples toward
-- zero until volume earns the number, and fades to irrelevance as real
-- denominators grow. Raw and shrunk are both exposed so the adjustment is
-- always visible and reversible.
-- =============================================================================
CREATE OR REPLACE VIEW silver.v_nfl_redzone_tiers AS
WITH pl AS (
    SELECT season, posteam AS team, player_id,
           ANY_VALUE(player_name) AS player_name,
           ANY_VALUE(opportunity_type) AS opportunity_type,
           count(*) FILTER (WHERE is_inside_20)                     AS opp_i20,
           sum(CASE WHEN is_inside_20 THEN yards_gained ELSE 0 END)  AS yds_i20,
           count(*) FILTER (WHERE is_inside_20 AND touchdown)        AS td_i20,
           count(*) FILTER (WHERE is_inside_10)                     AS opp_i10,
           sum(CASE WHEN is_inside_10 THEN yards_gained ELSE 0 END)  AS yds_i10,
           count(*) FILTER (WHERE is_inside_10 AND touchdown)        AS td_i10,
           count(*) FILTER (WHERE is_inside_5)                      AS opp_i5,
           sum(CASE WHEN is_inside_5 THEN yards_gained ELSE 0 END)   AS yds_i5,
           count(*) FILTER (WHERE is_inside_5 AND touchdown)         AS td_i5,
           count(*) FILTER (WHERE is_goal_to_go)                    AS opp_g2g,
           count(*) FILTER (WHERE is_goal_to_go AND touchdown)       AS td_g2g
    FROM silver.nfl_opportunity_event
    WHERE player_id IS NOT NULL
    GROUP BY season, posteam, player_id
), tm AS (
    SELECT season, posteam AS team,
           count(*) FILTER (WHERE is_inside_20)  AS t_i20,
           count(*) FILTER (WHERE is_inside_10)  AS t_i10,
           count(*) FILTER (WHERE is_inside_5)   AS t_i5,
           count(*) FILTER (WHERE is_goal_to_go) AS t_g2g
    FROM silver.nfl_opportunity_event
    GROUP BY season, posteam
)
SELECT pl.season, pl.team, pl.player_id, pl.player_name, pl.opportunity_type,
       tm.t_i20 AS team_opp_i20, tm.t_i10 AS team_opp_i10, tm.t_i5 AS team_opp_i5,
       pl.opp_i20, pl.yds_i20, pl.td_i20,
       round(1.0 * pl.opp_i20 / NULLIF(tm.t_i20, 0), 4)     AS pct_i20_intra,
       round(1.0 * pl.opp_i20 / NULLIF(tm.t_i20 + 4, 0), 4) AS pct_i20_shrunk,
       pl.opp_i10, pl.yds_i10, pl.td_i10,
       round(1.0 * pl.opp_i10 / NULLIF(tm.t_i10, 0), 4)     AS pct_i10_intra,
       round(1.0 * pl.opp_i10 / NULLIF(tm.t_i10 + 4, 0), 4) AS pct_i10_shrunk,
       pl.opp_i5, pl.yds_i5, pl.td_i5,
       round(1.0 * pl.opp_i5 / NULLIF(tm.t_i5, 0), 4)       AS pct_i5_intra,
       round(1.0 * pl.opp_i5 / NULLIF(tm.t_i5 + 4, 0), 4)   AS pct_i5_shrunk,
       pl.opp_g2g, pl.td_g2g,
       round(1.0 * pl.opp_g2g / NULLIF(tm.t_g2g + 4, 0), 4) AS pct_g2g_shrunk
FROM pl JOIN tm ON tm.season = pl.season AND tm.team = pl.team
WHERE pl.opp_i20 > 0;

-- Where on the field the ball went: pass location x air-yard depth for
-- targets, run location x gap for carries. One view, one row per bucket.
CREATE OR REPLACE VIEW silver.v_nfl_opportunity_location_map AS
SELECT season, posteam AS team, player_id,
       ANY_VALUE(player_name) AS player_name,
       opportunity_type,
       COALESCE(pass_location, run_location, 'unknown') AS field_side,
       CASE WHEN opportunity_type = 'rush' THEN COALESCE(run_gap, 'no_gap_recorded')
            WHEN air_yards IS NULL   THEN 'unknown'
            WHEN air_yards < 0       THEN 'behind_los'
            WHEN air_yards <= 5      THEN 'short_0_5'
            WHEN air_yards <= 10     THEN 'short_6_10'
            WHEN air_yards <= 20     THEN 'intermediate_11_20'
            ELSE 'deep_21_plus' END                    AS depth_or_gap,
       count(*)                                        AS opportunities,
       count(*) FILTER (WHERE is_high_stress)           AS opportunities_stress,
       sum(yards_gained)                               AS yards,
       count(*) FILTER (WHERE touchdown)                AS tds
FROM silver.nfl_opportunity_event
WHERE player_id IS NOT NULL
GROUP BY season, posteam, player_id, opportunity_type, field_side, depth_or_gap;

-- =============================================================================
-- LAYER 3 -- the depth chart, resolved point-in-time per game.
-- Kickoff derived the way ingest_rotowire_props.py does it: schedule gametime
-- is published in US/Eastern, converted to a real instant.
-- =============================================================================
CREATE OR REPLACE VIEW silver.v_nfl_depth_chart_pregame AS
WITH kick AS (
    SELECT game_id, season, week, home_team AS team,
           timezone('America/New_York', game_date + CAST(game_time_local AS TIME)) AS kickoff_utc
    FROM silver.nfl_schedule
    UNION ALL
    SELECT game_id, season, week, away_team,
           timezone('America/New_York', game_date + CAST(game_time_local AS TIME))
    FROM silver.nfl_schedule
), best AS (
    SELECT k.game_id, k.season, k.week, k.team, k.kickoff_utc,
           max(d.snapshot_at) AS snapshot_at
    FROM kick k
    JOIN silver.nfl_depth_chart_snapshot d
      ON d.team = k.team AND d.snapshot_at <= k.kickoff_utc
    GROUP BY k.game_id, k.season, k.week, k.team, k.kickoff_utc
)
SELECT b.game_id, b.season, b.week, b.team, b.kickoff_utc, b.snapshot_at,
       d.pos_abb, d.pos_name, d.pos_rank, d.gsis_id AS player_id, d.player_name
FROM best b
JOIN silver.nfl_depth_chart_snapshot d
  ON d.team = b.team AND d.snapshot_at = b.snapshot_at
WHERE d.pos_grp = '3WR 1TE'
  AND d.pos_abb IN ('QB', 'RB', 'WR', 'TE', 'FB');

-- =============================================================================
-- COMPONENTS -- all five layers, facts only, no weighting.
--
-- Kept separate from the indicator on purpose: these are observations. If the
-- weights below are ever retuned or discarded, nothing in this view moves.
-- =============================================================================
CREATE OR REPLACE VIEW gold.v_nfl_player_usage_components AS
WITH scrim AS (
    -- LAYER 5: Yards From Scrimmage. Reused, not recomputed.
    SELECT s.season, s.team, s.player_id,
           sum(s.touches)                                       AS touches,
           sum(s.scrimmage_yards)                               AS scrimmage_yards,
           sum(s.scrimmage_tds)                                 AS scrimmage_tds,
           round(1.0 * sum(s.scrimmage_yards)
                 / NULLIF(sum(s.touches), 0), 2)                AS yards_per_touch,
           round(1.0 * sum(s.scrimmage_yards)
                 / NULLIF(sum(t.team_scrimmage_yards), 0), 4)   AS scrimmage_yards_share_intra
    FROM silver.nfl_player_scrimmage_week s
    JOIN (SELECT season, game_id, team, sum(scrimmage_yards) AS team_scrimmage_yards
          FROM silver.nfl_player_scrimmage_week GROUP BY 1,2,3) t
      ON t.game_id = s.game_id AND t.team = s.team
    WHERE s.touches > 0
    GROUP BY s.season, s.team, s.player_id
), depth AS (
    SELECT season, team, player_id,
           arg_max(pos_abb,  week) AS depth_pos,
           arg_max(pos_rank, week) AS depth_rank,
           max(week)               AS depth_as_of_week
    FROM silver.v_nfl_depth_chart_pregame
    WHERE player_id IS NOT NULL
    GROUP BY season, team, player_id
)
SELECT ss.season, ss.team, ss.player_id,
       COALESCE(ss.player_name, p.player_display_name)  AS player_name,
       p.position, ss.games,
       -- LAYER 1: the spine
       ss.spine_metric, ss.spine_opps, ss.team_spine_opps, ss.share_overall,
       -- LAYER 2: stress
       ss.spine_opps_stress, ss.team_spine_opps_stress, ss.share_stress,
       ss.spine_opps_calm, ss.team_spine_opps_calm, ss.share_calm,
       ss.confidence_delta,
       -- LAYER 3: depth chart
       d.depth_pos, d.depth_rank, d.depth_as_of_week,
       -- LAYER 4: where
       rz.opp_i20, rz.opp_i10, rz.opp_i5, rz.opp_g2g,
       rz.td_i20, rz.td_i5,
       rz.team_opp_i20,
       rz.pct_i20_intra, rz.pct_i20_shrunk, rz.pct_i10_shrunk, rz.pct_i5_shrunk,
       rz.pct_g2g_shrunk,
       -- LAYER 5: return
       sc.touches, sc.scrimmage_yards, sc.yards_per_touch,
       sc.scrimmage_yards_share_intra, sc.scrimmage_tds,
       -- intra-team usage ranks. The pos-group rank is the ONLY one comparable
       -- to depth_rank, since depth_rank is itself within-position (WR1/WR2).
       -- Ranking a TE against the team's WRs and calling the gap a surplus was
       -- a real bug caught on first run: every TE1 and RB1 showed a large
       -- negative surplus purely because receivers out-target them.
       row_number() OVER (PARTITION BY ss.season, ss.team ORDER BY ss.spine_opps DESC)
                                                        AS usage_rank_on_team,
       row_number() OVER (PARTITION BY ss.season, ss.team, d.depth_pos
                          ORDER BY ss.spine_opps DESC)  AS usage_rank_in_pos_group
FROM silver.v_nfl_stress_split ss
LEFT JOIN silver.v_nfl_redzone_tiers rz
       ON rz.season = ss.season AND rz.team = ss.team AND rz.player_id = ss.player_id
LEFT JOIN scrim sc
       ON sc.season = ss.season AND sc.team = ss.team AND sc.player_id = ss.player_id
LEFT JOIN depth d
       ON d.season = ss.season AND d.team = ss.team AND d.player_id = ss.player_id
LEFT JOIN silver.nfl_player p ON p.player_id = ss.player_id;

-- =============================================================================
-- THE INDICATOR
--
-- READ THIS BEFORE TRUSTING THE NUMBER.
--
-- Everything above this line is an observed fact. This view is not. It is a
-- weighted heuristic. It has NOT been backtested, and no claim is made that a
-- high score predicts anything. Validating it -- does a rising score precede a
-- prop line moving -- is Phase 2 and needs a walk-forward test on
-- point-in-time data, not a correlation against this season's own totals.
--
-- The weights sit in one CTE so they change in one place, and every input is
-- exposed raw above so the score can always be taken apart.
--
-- STRESS CARRIES THE MOST WEIGHT because it is the user's actual thesis: the
-- tighter the situation, the more a coach goes to who he knows. Raw share is
-- volume, which a book already prices. The stress delta is the part that says
-- something a box score does not.
-- =============================================================================
CREATE OR REPLACE VIEW gold.v_nfl_player_usage_index AS
WITH weights AS (
    -- Change here and nowhere else. Sums to 1.0.
    SELECT 0.34::DOUBLE AS w_stress,   -- does his share GROW under pressure
           0.22::DOUBLE AS w_redzone,  -- share of the scarcest chances
           0.20::DOUBLE AS w_share,    -- baseline intra-team opportunity share
           0.12::DOUBLE AS w_depth,    -- out-earning his listed depth slot
           0.12::DOUBLE AS w_return    -- what happens once he has it
), base AS (
    SELECT c.*,
           CASE WHEN c.depth_rank IS NULL THEN NULL
                ELSE c.depth_rank - c.usage_rank_in_pos_group END AS depth_surplus,
           -- Y/Tch scaled within his own team: "efficient" must mean efficient
           -- against the teammates competing for the same touches, not against
           -- the league, which would reintroduce the cross-team comparison
           -- this pond exists to avoid.
           CASE WHEN max(c.yards_per_touch) OVER w = min(c.yards_per_touch) OVER w THEN 0.5
                ELSE (c.yards_per_touch - min(c.yards_per_touch) OVER w)
                     / NULLIF(max(c.yards_per_touch) OVER w
                              - min(c.yards_per_touch) OVER w, 0) END AS ypt_scaled_in_team,
           -- Shrink the stress delta by how many high-stress chances it rests
           -- on. A +40% delta off 2 opportunities is not a finding. At 8+
           -- high-stress opportunities this is essentially unshrunk.
           COALESCE(c.confidence_delta, 0)
             * (COALESCE(c.spine_opps_stress, 0)::DOUBLE
                / (COALESCE(c.spine_opps_stress, 0) + 6))          AS confidence_delta_shrunk
    FROM gold.v_nfl_player_usage_components c
    WINDOW w AS (PARTITION BY c.season, c.team)
), scored AS (
    SELECT b.*,
           -- stress axis: 0 delta -> 50. +0.20 (twenty points more share when
           -- tight) -> 100. Symmetric downward, so a coach visibly taking the
           -- ball OUT of someone's hands under pressure scores below 50.
           round(LEAST(GREATEST(50 + 250 * b.confidence_delta_shrunk, 0), 100), 1)
                                                                   AS stress_score,
           round(LEAST(100 * (0.5 * COALESCE(b.pct_i20_shrunk, 0)
                              + 0.3 * COALESCE(b.pct_i10_shrunk, 0)
                              + 0.2 * COALESCE(b.pct_i5_shrunk, 0)) / 0.32, 100), 1)
                                                                   AS redzone_score,
           round(LEAST(100 * COALESCE(b.share_overall, 0) / 0.35, 100), 1)
                                                                   AS share_score,
           round(50 + 50 * LEAST(GREATEST(COALESCE(b.depth_surplus, 0), -3), 3) / 3.0, 1)
                                                                   AS depth_score,
           round(LEAST(100 * (0.5 * COALESCE(b.ypt_scaled_in_team, 0)
                              + 0.5 * COALESCE(b.scrimmage_yards_share_intra, 0) / 0.40),
                       100), 1)                                    AS return_score
    FROM base b
)
SELECT s.season, s.team, s.player_id, s.player_name, s.position, s.games,
       s.spine_metric,
       round(w.w_stress  * s.stress_score
           + w.w_redzone * s.redzone_score
           + w.w_share   * s.share_score
           + w.w_depth   * s.depth_score
           + w.w_return  * s.return_score, 1)                      AS usage_index_score,
       -- the axes, readable on their own
       s.stress_score, s.redzone_score, s.share_score, s.depth_score, s.return_score,
       -- LAYER 2 raw: the actual thesis, in plain numbers
       s.share_calm, s.share_stress, s.confidence_delta,
       s.spine_opps_stress, s.team_spine_opps_stress,
       -- the classification. A single score collapses cases a bettor must
       -- treat completely differently, so the role travels with it.
       CASE
         WHEN s.spine_opps < 5                                THEN 'INSUFFICIENT SAMPLE'
         WHEN COALESCE(s.depth_surplus, 0) >= 1
              AND s.share_score >= 20                          THEN 'RISING -- OUT-EARNING DEPTH CHART'
         WHEN s.stress_score >= 60 AND s.share_score >= 40     THEN 'PRIMARY -- TRUSTED UNDER PRESSURE'
         WHEN s.stress_score >= 60 AND s.share_score <  40     THEN 'CLOSER -- SMALL ROLE, BIG MOMENTS'
         WHEN s.stress_score <  40 AND s.share_score >= 40     THEN 'VOLUME ONLY -- BENCHED WHEN IT TIGHTENS'
         WHEN s.redzone_score >= 45 AND s.share_score < 40      THEN 'RED ZONE SPECIALIST'
         WHEN s.share_score >= 20                              THEN 'ROTATIONAL CONTRIBUTOR'
         ELSE 'FRINGE'
       END                                                        AS usage_role,
       -- facts carried through so the score is never taken on faith
       s.spine_opps, s.team_spine_opps, s.share_overall,
       s.usage_rank_on_team, s.usage_rank_in_pos_group,
       s.depth_pos, s.depth_rank, s.depth_surplus, s.depth_as_of_week,
       s.opp_i20, s.opp_i10, s.opp_i5, s.opp_g2g, s.team_opp_i20,
       s.pct_i20_intra, s.pct_i20_shrunk, s.td_i20, s.td_i5,
       s.touches, s.yards_per_touch, s.scrimmage_yards,
       s.scrimmage_yards_share_intra, s.scrimmage_tds
FROM scored s CROSS JOIN weights w
ORDER BY usage_index_score DESC;

-- =============================================================================
-- "Who is THIS coach's son" -- one team at a time, ranked within itself.
-- The pond's whole purpose.
-- =============================================================================
CREATE OR REPLACE VIEW gold.v_nfl_player_usage_by_team AS
SELECT team, season,
       row_number() OVER (PARTITION BY team, season ORDER BY usage_index_score DESC)
                                                        AS rank_on_team,
       player_name, position, spine_metric,
       depth_pos, depth_rank, depth_surplus,
       usage_index_score, usage_role,
       stress_score, redzone_score, share_score, depth_score, return_score,
       spine_opps, team_spine_opps, share_overall,
       share_calm, share_stress, confidence_delta,
       opp_i20, team_opp_i20, yards_per_touch, scrimmage_yards
FROM gold.v_nfl_player_usage_index
ORDER BY team, rank_on_team;

-- =============================================================================
-- CORRELATION: usage index x the TD log
--
-- The user's framing, and the reason the TD logs were built before this pond:
--
-- The TD log exists upstream of this pond for exactly this join: it is the
-- confirmation step for the usage read. Related stats must agree -- if the
-- usage index is right, the scoring log should echo it.
--
-- The usage index says who the coach LEANS ON.
-- silver.nfl_td_log says who actually FINISHED. Putting them side by side turns
-- an opinion into a testable claim:
--
--   agreement   -- high confidence AND scoring. The read holds.
--   opportunity -- high confidence, no TDs yet. The coach keeps going back to
--                  him and it has not cashed. Anytime-TD markets price the
--                  result, not the usage, so this is where a line lags.
--   hollow      -- TDs without confidence. Scored on volume or a broken play,
--                  not on trust. The most dangerous profile to bet on repeating,
--                  which is exactly the profile a book will advertise.
--
-- Both sides are already in this lake and both are facts. The only judgement
-- here is the label, and the counts that produce it travel with it.
-- =============================================================================
CREATE OR REPLACE VIEW gold.v_nfl_player_usage_td_correlation AS
WITH td AS (
    SELECT season, scorer_player_id AS player_id,
           ANY_VALUE(scoring_team)                          AS team,
           count(*)                                          AS tds,
           count(*) FILTER (WHERE is_red_zone)               AS tds_red_zone,
           count(*) FILTER (WHERE is_goal_to_go)             AS tds_goal_to_go,
           count(*) FILTER (WHERE is_third_down)             AS tds_third_down,
           count(*) FILTER (WHERE td_type = 'rush')          AS tds_rush,
           count(*) FILTER (WHERE td_type = 'pass')          AS tds_rec,
           count(DISTINCT game_id)                           AS games_with_td
    FROM silver.nfl_td_log
    WHERE scorer_player_id IS NOT NULL
    GROUP BY season, scorer_player_id
)
SELECT cs.season, cs.team, cs.player_id, cs.player_name, cs.position,
       cs.spine_metric, cs.usage_index_score, cs.usage_role,
       -- the confidence side
       cs.share_overall, cs.share_calm, cs.share_stress, cs.confidence_delta,
       cs.opp_i20, cs.opp_i10, cs.opp_i5, cs.opp_g2g,
       cs.pct_i20_shrunk,
       -- the finishing side (TD log)
       COALESCE(td.tds, 0)             AS tds,
       COALESCE(td.tds_red_zone, 0)    AS tds_red_zone,
       COALESCE(td.tds_goal_to_go, 0)  AS tds_goal_to_go,
       COALESCE(td.tds_rush, 0)        AS tds_rush,
       COALESCE(td.tds_rec, 0)         AS tds_rec,
       COALESCE(td.games_with_td, 0)   AS games_with_td,
       -- conversion: TDs per red-zone opportunity. The bridge between the two.
       round(1.0 * COALESCE(td.tds_red_zone, 0) / NULLIF(cs.opp_i20, 0), 3)
                                       AS td_per_redzone_opp,
       -- IF THIS THEN THAT: does the confidence read agree with the scoring?
       CASE
         WHEN cs.usage_index_score >= 55 AND COALESCE(td.tds, 0) >= 2
              THEN 'AGREEMENT -- trusted and finishing'
         WHEN cs.usage_index_score >= 55 AND COALESCE(td.tds, 0) = 0
              THEN 'OPPORTUNITY -- trusted, has not cashed yet'
         WHEN cs.usage_index_score >= 55
              THEN 'AGREEMENT -- trusted, finishing lightly'
         WHEN cs.usage_index_score < 40 AND COALESCE(td.tds, 0) >= 2
              THEN 'HOLLOW -- scoring without trust, fragile'
         WHEN COALESCE(td.tds, 0) = 0 AND cs.usage_index_score < 40
              THEN 'QUIET -- neither trusted nor scoring'
         ELSE 'MIXED'
       END                             AS correlation_flag
FROM gold.v_nfl_player_usage_index cs
LEFT JOIN td ON td.season = cs.season AND td.player_id = cs.player_id
WHERE cs.usage_role <> 'INSUFFICIENT SAMPLE'
ORDER BY cs.usage_index_score DESC;