Timeline alignment
Core age fit, prime-window alignment, development environment, and continuity. This pillar explicitly reads mandate-specific targets; its raw evaluation is not mandate-independent.
Aggregation
The table below is extracted from the pillar's declared metric weights at build time. Open the metric implementation notes and source to inspect its raw inputs, normalization, branches, and fallback handling.
pillar_score = Σ(metric_score × metric_weight)
Metric weights
| Metric | Declared weight |
|---|---|
| Core age fit | 30.0% |
| Prime window alignment | 25.0% |
| Core continuity | 25.0% |
| Development environment | 20.0% |
Implementation
These notes and source files are captured from this deployment's code. Expand a module for its algorithm and branches, or download the Elixir file for local inspection.
timeline_alignment.exElixir
lib/trade_machine/gm_grade_v2/pillars/timeline_alignment.ex
Aggregates AVC's four mandate-dependent Timeline Alignment metrics.
A valid 240-minute rotation is authoritative input. Missing or invalid rotations produce a provisional pillar with no component scores.
SHA-256 1f330da79d797d7258820542d09053f07f316e9509706cf37402a70f8c137ee3
defmodule TradeMachine.GMGradeV2.Pillars.TimelineAlignment do
@moduledoc """
Aggregates AVC's four mandate-dependent Timeline Alignment metrics.
A valid 240-minute rotation is authoritative input. Missing or invalid
rotations produce a provisional pillar with no component scores.
"""
alias TradeMachine.GMGradeV2.Benchmark
alias TradeMachine.GMGradeV2.ChallengeSnapshot
alias TradeMachine.GMGradeV2.ControlProjection
alias TradeMachine.GMGradeV2.CoreEvaluator
alias TradeMachine.GMGradeV2.MandateProfile
alias TradeMachine.GMGradeV2.Pillars.CompetitiveStrength.MetricInputs
alias TradeMachine.GMGradeV2.Pillars.ContractHealth.Shared
alias TradeMachine.GMGradeV2.Pillars.TimelineAlignment.CoreAgeFit
alias TradeMachine.GMGradeV2.Pillars.TimelineAlignment.CoreContinuity
alias TradeMachine.GMGradeV2.Pillars.TimelineAlignment.DevelopmentEnvironment
alias TradeMachine.GMGradeV2.Pillars.TimelineAlignment.PrimeWindowAlignment
alias TradeMachine.GMGradeV2.Result.MetricResult
alias TradeMachine.GMGradeV2.Result.PillarResult
alias TradeMachine.GMGradeV2.RotationValidation
alias TradeMachine.GMGradeV2.ScenarioValidation
@metric_weights %{
core_age_fit: 0.30,
prime_window_alignment: 0.25,
core_continuity: 0.25,
development_environment: 0.20
}
if abs(Enum.sum(Map.values(@metric_weights)) - 1.0) > 1.0e-9 do
raise "Timeline Alignment metric weights must sum to 1.0"
end
@doc """
Scores a scenario against the challenge's frozen timeline evidence.
The snapshot carries player impacts, birth dates, season start date, and
mandate profile; its benchmark carries the league adjusted-EPM population.
Control projections are composed from `ScenarioState`'s public player/payroll
facts, while tests and other pure callers may provide precomputed projections.
"""
@spec score(map(), ChallengeSnapshot.t()) :: PillarResult.t()
def score(
scenario_state,
%ChallengeSnapshot{benchmark: %Benchmark{} = benchmark} = snapshot
)
when is_map(scenario_state) do
with {:ok, profile} <- mandate_profile(scenario_state, snapshot),
{:ok, entries} <- valid_entries(scenario_state),
{:ok, inputs} <- timeline_inputs(scenario_state, snapshot, benchmark, profile) do
evaluate(entries, inputs, profile, benchmark.version)
else
{:error, {:mandate_profile, reason}} ->
provisional(benchmark.version, nil, reason)
{:error, reason} ->
profile =
case mandate_profile(scenario_state, snapshot) do
{:ok, profile} -> profile
_ -> nil
end
provisional(benchmark.version, profile, reason)
end
end
@doc "Combines the four metric results at AVC's fixed 30/25/25/20 weights."
@spec weighted_score([MetricResult.t()]) :: float()
def weighted_score(results) do
Enum.reduce(results, 0.0, fn result, total ->
total + result.score * Map.fetch!(@metric_weights, result.key)
end)
end
defp evaluate(entries, inputs, profile, version) do
case CoreEvaluator.evaluate(
entries,
inputs.players,
inputs.players,
inputs.season_start,
inputs.league_epms
) do
{:ok, evaluation} ->
metrics =
[
CoreAgeFit.score(evaluation.core, profile),
PrimeWindowAlignment.score(evaluation.core, profile),
CoreContinuity.score(
evaluation.core,
profile,
inputs.control_projection.projections
),
DevelopmentEnvironment.score(evaluation.rotation_players, profile)
]
|> Enum.map(&attach_evaluation_metadata(&1, evaluation, inputs.control_projection))
{strongest, weakest} = extrema(metrics)
%PillarResult{
key: :timeline_alignment,
version: version,
score: weighted_score(metrics),
metric_results: metrics,
metric_weights: @metric_weights,
mandate_profile: profile_echo(profile),
explanation:
"Timeline Alignment applies #{profile.label}'s targets; " <>
"#{label(strongest.key)} is strongest and #{label(weakest.key)} is weakest.",
strongest: strongest.key,
weakest: weakest.key
}
{:error, :degenerate_core} ->
provisional(version, profile, "the eligible core has zero total importance.")
end
end
defp timeline_inputs(scenario_state, snapshot, benchmark, profile) do
with {:ok, season_start} <- season_start(snapshot),
{:ok, league_epms} <- league_adjusted_epms(benchmark),
{:ok, control_projection} <- control_projections(scenario_state, profile) do
{:ok,
%{
players: ChallengeSnapshot.section(snapshot, :players),
season_start: season_start,
league_epms: league_epms,
control_projection: control_projection
}}
end
end
defp attach_evaluation_metadata(metric, evaluation, control_projection) do
metric =
%{
metric
| fallbacks: metric.fallbacks ++ evaluation.fallbacks,
confidence: Map.merge(metric.confidence, evaluation.confidence),
diagnostics: Map.put(metric.diagnostics, :core_evaluation, evaluation.diagnostics)
}
if metric.key == :core_continuity do
%{
metric
| fallbacks: metric.fallbacks ++ control_projection.fallbacks,
confidence: Map.merge(metric.confidence, control_projection.confidence)
}
else
metric
end
end
defp valid_entries(scenario_state) do
case Map.get(scenario_state, :rotation) do
nil ->
{:error, "the scenario has no 240-minute rotation."}
rotation ->
entries = MetricInputs.rotation_entries(rotation)
roster_ids =
scenario_state
|> Map.get(:roster, [])
|> ScenarioValidation.rotation_eligible_player_ids()
injured_ids =
scenario_state
|> Map.get(:roster, [])
|> ScenarioValidation.out_for_season_player_ids()
case RotationValidation.validate(entries, roster_ids, injured_ids) do
{:ok, normalized} ->
{:ok, normalized}
{:error, errors} ->
{:error,
"the scenario rotation is invalid. " <> Enum.map_join(errors, " ", & &1.message)}
end
end
end
defp mandate_profile(%{mandate_profile: %MandateProfile{} = profile}, _snapshot),
do: {:ok, profile}
defp mandate_profile(_scenario_state, snapshot) do
stored = ChallengeSnapshot.section(snapshot, :mandate_profile)
case MandateProfile.from_frozen(stored) do
{:ok, profile} ->
{:ok, profile}
_ ->
{:error, {:mandate_profile, "the frozen mandate profile is incomplete or unknown."}}
end
end
defp season_start(snapshot) do
value =
snapshot.data
|> Map.get("season", %{})
|> Map.get("start_date")
case value do
%Date{} = value ->
{:ok, value}
value when is_binary(value) ->
case Date.from_iso8601(value) do
{:ok, date} -> {:ok, date}
{:error, _reason} -> {:error, "the frozen season-start date is invalid."}
end
_ ->
{:error, "the frozen season-start date is missing."}
end
end
defp league_adjusted_epms(benchmark) do
with %{} = skills <- Benchmark.section(benchmark, :skills),
%{} = tables <- Map.get(skills, "player_percentiles"),
values when is_list(values) and values != [] <- Map.get(tables, "adjusted_epm") do
{:ok, values}
else
_ -> {:error, "the frozen league adjusted-EPM population is missing."}
end
end
defp control_projections(%{control_projections: projections}, _profile)
when is_map(projections) do
{:ok, %{projections: projections, fallbacks: [], confidence: %{control_inputs: :complete}}}
end
defp control_projections(scenario_state, profile) do
with {:ok, seasons} <- horizon_seasons(scenario_state, profile.continuity_horizon),
contracts when is_list(contracts) <- Shared.active_contracts(scenario_state),
true <- contracts != [] do
contract_index =
Map.new(contracts, fn contract ->
{fetch_value(contract, :player_id), contract}
end)
{projections, fallbacks} =
scenario_state
|> Map.get(:roster, [])
|> Enum.map_reduce([], fn roster_player, fallbacks ->
player_id = Map.get(roster_player, :id) || Map.get(roster_player, "id")
contract = Map.get(contract_index, player_id)
case contract && Shared.source_player(scenario_state, contract) do
nil ->
projection = ControlProjection.project(%{id: player_id}, seasons)
fallback = %{
input: player_id,
method: :missing_control_input,
note:
"No active scenario contract could be resolved; continuity uses no-control statuses."
}
{{player_id, projection}, [fallback | fallbacks]}
source_player ->
{control_player, modifications} =
Shared.control_projection_input(
source_player,
contract,
Map.get(scenario_state, :modifications, %{})
)
projection =
ControlProjection.project(
control_player,
seasons,
modifications: modifications
)
{{player_id, projection}, fallbacks}
end
end)
fallbacks = Enum.reverse(fallbacks)
{:ok,
%{
projections: Map.new(projections),
fallbacks: fallbacks,
confidence: %{
control_inputs: if(fallbacks == [], do: :complete, else: :reduced),
unresolved_control_player_count: length(fallbacks)
}
}}
else
_ -> {:error, "the scenario control-projection inputs are missing."}
end
end
defp horizon_seasons(scenario_state, horizon) do
graded_season = Map.get(scenario_state, :season)
with %{seasons: seasons} when is_list(seasons) <-
Map.get(scenario_state, :payroll_projection),
index when is_integer(index) <- Enum.find_index(seasons, &(&1 == graded_season)),
selected when length(selected) == horizon <- Enum.slice(seasons, index, horizon) do
{:ok, selected}
else
_ -> {:error, :missing_horizon}
end
end
defp provisional(version, profile, reason) do
%PillarResult{
key: :timeline_alignment,
version: version,
score: 0.0,
metric_results: [],
metric_weights: @metric_weights,
provisional?: true,
mandate_profile: profile_echo(profile),
explanation: "Timeline Alignment is provisional and was not calculated because #{reason}"
}
end
defp profile_echo(nil), do: nil
defp profile_echo(profile) do
Map.take(Map.from_struct(profile), [
:key,
:version,
:ideal_core_age,
:age_penalty_below,
:age_penalty_above,
:prime_offset_range,
:continuity_horizon,
:young_minute_share_range,
:veteran_count_range,
:young_role_range
])
end
defp extrema(results), do: {Enum.max_by(results, & &1.score), Enum.min_by(results, & &1.score)}
defp fetch_value(map, key), do: Map.get(map, key, Map.get(map, Atom.to_string(key)))
defp label(:core_age_fit), do: "Core Age Fit"
defp label(:prime_window_alignment), do: "Prime Window Alignment"
defp label(:core_continuity), do: "Core Continuity"
defp label(:development_environment), do: "Development Environment"
end
core_age_fit.exElixir
lib/trade_machine/gm_grade_v2/pillars/timeline_alignment/core_age_fit.ex
Scores each core player's whole-year age against the selected mandate.
SHA-256 7a7d5cf4e090462dd12c360ef50cee938dfc55ddb4ddf64c9b260b4d4328c6f8
defmodule TradeMachine.GMGradeV2.Pillars.TimelineAlignment.CoreAgeFit do
@moduledoc "Scores each core player's whole-year age against the selected mandate."
alias TradeMachine.GMGradeV2.MandateProfile
alias TradeMachine.GMGradeV2.Result.MetricResult
@doc "Returns AVC's core-weighted Core Age Fit metric."
@spec score([map()], MandateProfile.t()) :: MetricResult.t()
def score(core, %MandateProfile{} = profile) when is_list(core) do
players =
Enum.map(core, fn player ->
player_score = player_score(player.age, profile)
%{
player_id: player.player_id,
age: player.age,
core_weight: player.core_weight,
score: player_score
}
end)
score = Enum.reduce(players, 0.0, &(&1.score * &1.core_weight + &2))
%MetricResult{
key: :core_age_fit,
version: profile.version,
score: score,
inputs: %{mandate: profile.key},
supporting: %{
players: players,
ideal_core_age: profile.ideal_core_age,
penalty_below: profile.age_penalty_below,
penalty_above: profile.age_penalty_above
},
weights: Map.new(players, &{&1.player_id, &1.core_weight}),
explanation: "Core players are scored against #{profile.label}'s ideal age range."
}
end
@doc "Scores one whole-year age against a mandate's asymmetric range."
@spec player_score(number(), MandateProfile.t()) :: float()
def player_score(age, %MandateProfile{} = profile) when is_number(age) do
penalty =
cond do
age < profile.ideal_core_age.min ->
(profile.ideal_core_age.min - age) * profile.age_penalty_below
age > profile.ideal_core_age.max ->
(age - profile.ideal_core_age.max) * profile.age_penalty_above
true ->
0
end
max(0.0, 100.0 - penalty)
end
end
core_continuity.exElixir
lib/trade_machine/gm_grade_v2/pillars/timeline_alignment/core_continuity.ex
Scores control of the six-player core through a mandate-specific horizon.
The input projections are normalized facts from
ControlProjection
; this
module only applies AVC's shared factors and continuity weights.
SHA-256 e83c148e7289a21eaa8d61fcf7dcfb15a0989fea220b8153119abf97c9453d2d
defmodule TradeMachine.GMGradeV2.Pillars.TimelineAlignment.CoreContinuity do
@moduledoc """
Scores control of the six-player core through a mandate-specific horizon.
The input projections are normalized facts from `ControlProjection`; this
module only applies AVC's shared factors and continuity weights.
"""
alias TradeMachine.GMGradeV2.ControlFactors
alias TradeMachine.GMGradeV2.MandateProfile
alias TradeMachine.GMGradeV2.Pillars.TimelineAlignment.CoreAgeFit
alias TradeMachine.GMGradeV2.Result.MetricResult
@doc """
Scores control projections keyed by player id.
Each projection may be either an ordered list of season evidence or a map
keyed by season. Exactly the first `continuity_horizon` seasons are scored.
"""
@spec score([map()], MandateProfile.t(), map()) :: MetricResult.t()
def score(core, %MandateProfile{} = profile, projections)
when is_list(core) and is_map(projections) do
factors = ControlFactors.all()
players =
Enum.map(core, fn player ->
seasons =
projections
|> fetch_projection(player.player_id)
|> ordered_seasons()
|> Enum.take(profile.continuity_horizon)
if length(seasons) != profile.continuity_horizon do
raise ArgumentError,
"control projection for #{inspect(player.player_id)} must cover " <>
"#{profile.continuity_horizon} seasons"
end
horizon_table =
Enum.map(seasons, fn season ->
status = normalize_status(fetch!(season, :status))
Map.put(season, :control_factor, Map.fetch!(factors, status))
end)
continuity =
horizon_table
|> Enum.reduce(0.0, &(fetch!(&1, :control_factor) + &2))
|> Kernel./(profile.continuity_horizon)
|> Kernel.*(100)
age_fit = CoreAgeFit.player_score(player.age, profile)
timeline_raw = player.core_weight * (0.40 + 0.60 * age_fit / 100)
%{
player_id: player.player_id,
core_importance_value: player.core_importance_value,
original_core_weight: player.core_weight,
age_fit_score: age_fit,
timeline_weight_raw: timeline_raw,
continuity_score: continuity,
horizon: horizon_table
}
end)
timeline_total = Enum.reduce(players, 0.0, &(&1.timeline_weight_raw + &2))
players =
Enum.map(players, fn player ->
Map.put(player, :timeline_relevant_weight, player.timeline_weight_raw / timeline_total)
end)
overall =
Enum.reduce(players, 0.0, &(&1.continuity_score * &1.timeline_relevant_weight + &2))
top_three = Enum.take(players, 3)
top_weight = Enum.reduce(top_three, 0.0, &(&1.original_core_weight + &2))
top_end =
Enum.reduce(top_three, 0.0, fn player, total ->
total + player.continuity_score * player.original_core_weight / top_weight
end)
%MetricResult{
key: :core_continuity,
version: profile.version,
score: overall * 0.70 + top_end * 0.30,
inputs: %{mandate: profile.key, continuity_horizon: profile.continuity_horizon},
supporting: %{
players: players,
overall_core_control: overall,
top_end_control: top_end
},
weights: %{overall_core_control: 0.70, top_end_control: 0.30},
fallbacks: estimated_control_fallbacks(players),
confidence: %{
estimated_control_seasons:
Enum.sum(
for player <- players do
Enum.count(player.horizon, &fetch(&1, :estimated, false))
end
)
},
explanation:
"Core Continuity measures franchise control through the #{profile.continuity_horizon}-season mandate window."
}
end
defp fetch_projection(projections, player_id) do
Map.get(projections, player_id) || Map.fetch!(projections, to_string(player_id))
end
defp ordered_seasons(projection) when is_list(projection), do: projection
defp ordered_seasons(projection) when is_map(projection) do
projection
|> Enum.sort_by(fn {season, _evidence} -> season end)
|> Enum.map(&elem(&1, 1))
end
defp normalize_status(status) when is_atom(status), do: status
defp normalize_status(status) when is_binary(status), do: String.to_existing_atom(status)
defp fetch!(map, key) do
case Map.fetch(map, key) do
{:ok, value} -> value
:error -> Map.fetch!(map, Atom.to_string(key))
end
end
defp estimated_control_fallbacks(players) do
for player <- players,
season <- player.horizon,
fetch(season, :estimated, false) == true do
%{
input: player.player_id,
method: :estimated_rfa_rights,
note: "Control status for #{fetch!(season, :season)} uses the documented RFA estimate."
}
end
end
defp fetch(map, key, default) do
Map.get(map, key, Map.get(map, Atom.to_string(key), default))
end
end
development_environment.exElixir
lib/trade_machine/gm_grade_v2/pillars/timeline_alignment/development_environment.ex
Scores youth opportunity, veteran infrastructure, and developmental roles.
All threshold comparisons are inclusive and intentionally preserve AVC's cliffs for the Phase 16 sensitivity lab.
SHA-256 1f666082e1a4de835bd96489fffc67970bf161926c3a6a9c556ebfff40a9bb2d
defmodule TradeMachine.GMGradeV2.Pillars.TimelineAlignment.DevelopmentEnvironment do
@moduledoc """
Scores youth opportunity, veteran infrastructure, and developmental roles.
All threshold comparisons are inclusive and intentionally preserve AVC's
cliffs for the Phase 16 sensitivity lab.
"""
alias TradeMachine.GMGradeV2.MandateProfile
alias TradeMachine.GMGradeV2.Result.MetricResult
@rotation_minutes 240
@doc "Returns AVC's 45/35/20 Development Environment metric."
@spec score([map()], MandateProfile.t()) :: MetricResult.t()
def score(players, %MandateProfile{} = profile) when is_list(players) do
young = Enum.filter(players, &(&1.age <= 24))
young_minutes = Enum.reduce(young, 0, &(&1.assigned_minutes + &2))
young_share = young_minutes / @rotation_minutes
veteran_count =
Enum.count(players, fn player ->
player.age >= 27 and player.assigned_minutes >= 12 and
is_number(player.season_minutes) and player.season_minutes >= 500 and
player.adjusted_epm >= -1.5
end)
young_role_count = Enum.count(young, &(&1.assigned_minutes >= 8))
opportunity = opportunity_score(young_share, profile.young_minute_share_range)
infrastructure = count_score(veteran_count, profile.veteran_count_range, 25, 10)
distribution = count_score(young_role_count, profile.young_role_range, 20, 10)
%MetricResult{
key: :development_environment,
version: profile.version,
score: combine(opportunity, infrastructure, distribution),
inputs: %{mandate: profile.key, rotation_minutes: @rotation_minutes},
supporting: %{
young_player_opportunity: opportunity,
veteran_infrastructure: infrastructure,
developmental_role_distribution: distribution,
young_rotation_minutes: young_minutes,
young_minute_share: young_share,
established_veteran_count: veteran_count,
meaningful_young_role_count: young_role_count,
targets: %{
young_minute_share: profile.young_minute_share_range,
veteran_count: profile.veteran_count_range,
young_roles: profile.young_role_range
}
},
weights: %{
young_player_opportunity: 0.45,
veteran_infrastructure: 0.35,
developmental_role_distribution: 0.20
},
explanation:
"Development Environment balances mandate-appropriate youth minutes, veterans, and meaningful young roles."
}
end
@doc "Combines AVC's three development sub-scores at 45/35/20."
def combine(opportunity, infrastructure, distribution) do
opportunity * 0.45 + infrastructure * 0.35 + distribution * 0.20
end
@doc "Scores young-minute share, applying two points per percentage point outside."
def opportunity_score(share, %{min: minimum, max: maximum}) when is_number(share) do
percentage_points =
cond do
share < minimum -> (minimum - share) * 100
share > maximum -> (share - maximum) * 100
true -> 0
end
max(0.0, 100.0 - 2 * percentage_points)
end
@doc "Scores an integer target range with asymmetric missing/excess penalties."
def count_score(count, %{min: minimum, max: maximum}, missing_penalty, excess_penalty) do
penalty =
cond do
count < minimum -> (minimum - count) * missing_penalty
count > maximum -> (count - maximum) * excess_penalty
true -> 0
end
max(0.0, 100.0 - penalty)
end
end
prime_window_alignment.exElixir
lib/trade_machine/gm_grade_v2/pillars/timeline_alignment/prime_window_alignment.ex
Scores mandate window timing and weighted core synchronization.
SHA-256 60d5beaf6e0541be23aec322d07d89bda9bebcc3b2cb9e42f2a0b9d29b8a0576
defmodule TradeMachine.GMGradeV2.Pillars.TimelineAlignment.PrimeWindowAlignment do
@moduledoc "Scores mandate window timing and weighted core synchronization."
alias TradeMachine.GMGradeV2.MandateProfile
alias TradeMachine.GMGradeV2.Result.MetricResult
@prime_midpoint 27.5
@curve [{1.5, 100.0}, {2.5, 85.0}, {3.5, 65.0}, {4.5, 40.0}, {6.0, 10.0}]
@doc "Returns AVC's 65/35 Prime Window Alignment metric."
@spec score([map()], MandateProfile.t()) :: MetricResult.t()
def score(core, %MandateProfile{} = profile) when is_list(core) do
players =
Enum.map(core, fn player ->
offset = @prime_midpoint - player.age
%{
player_id: player.player_id,
age: player.age,
core_weight: player.core_weight,
prime_timing_offset: offset,
window_timing_score: timing_score(offset, profile.prime_offset_range)
}
end)
timing = Enum.reduce(players, 0.0, &(&1.window_timing_score * &1.core_weight + &2))
spread = weighted_population_sd(players)
synchronization = synchronization_score(spread)
final = combine(timing, synchronization)
%MetricResult{
key: :prime_window_alignment,
version: profile.version,
score: final,
inputs: %{mandate: profile.key, prime_midpoint: @prime_midpoint},
supporting: %{
players: players,
prime_offset_range: profile.prime_offset_range,
window_timing: timing,
prime_timing_spread: spread,
core_synchronization: synchronization
},
weights: %{window_timing: 0.65, core_synchronization: 0.35},
explanation: "Prime Window Alignment combines mandate timing and core synchronization."
}
end
@doc "Combines Window Timing and Core Synchronization at AVC's 65/35 weights."
def combine(window_timing, core_synchronization) do
window_timing * 0.65 + core_synchronization * 0.35
end
@doc "Scores a prime offset against the selected mandate range."
def timing_score(offset, %{min: minimum, max: maximum}) when is_number(offset) do
distance =
if offset < minimum, do: minimum - offset, else: Kernel.max(offset - maximum, 0)
max(0.0, 100.0 - 15 * distance)
end
@doc "Maps weighted population spread onto AVC's piecewise-linear curve."
def synchronization_score(spread) when spread <= 1.5, do: 100.0
def synchronization_score(spread) when spread >= 6.0, do: 10.0
def synchronization_score(spread) do
[{x1, y1}, {x2, y2}] =
@curve
|> Enum.chunk_every(2, 1, :discard)
|> Enum.find(fn [{x1, _}, {x2, _}] -> spread >= x1 and spread <= x2 end)
y1 + (spread - x1) / (x2 - x1) * (y2 - y1)
end
defp weighted_population_sd(players) do
mean =
Enum.reduce(players, 0.0, &(&1.prime_timing_offset * &1.core_weight + &2))
variance =
Enum.reduce(players, 0.0, fn player, total ->
delta = player.prime_timing_offset - mean
total + player.core_weight * delta * delta
end)
:math.sqrt(variance)
end
end