feature: NEWS2 Composite Scoring Engine

This commit is contained in:
voltsrage
2026-06-18 17:39:31 +08:00
parent c3fbc20ddc
commit e6f7989298
31 changed files with 3118 additions and 45 deletions
@@ -0,0 +1,105 @@
using StackExchange.Redis;
public static class News2Calculator
{
// The 7 NEWS2 parameter codes. Order is stable for MGET.
public static readonly IReadOnlyList<string> ParameterCodes = new[]
{
"RESP_RATE", "SPO2", "SYSTOLIC_BP", "HEART_RATE", "AVPU", "TEMP_C", "SUPPLEMENTAL_O2"
};
public static RedisKey[] AllParameterKeys(Guid encounterId) =>
ParameterCodes
.Select(code => (RedisKey)$"news2:{encounterId}:{code}")
.ToArray();
public static string ParameterKey(Guid encounterId, string code) =>
$"news2:{encounterId}:{code}";
public static bool IsNews2Code(string observationCode) =>
ParameterCodes.Contains(observationCode);
// --- Individual parameter scoring ---
// Each method returns 0-3 per the official NEWS2 scoring table.
public static int ScoreRespRate(decimal value) => value switch
{
<= 8 => 3,
<= 11 => 1,
<= 20 => 0,
<= 24 => 2,
_ => 3 // >= 25
};
// Scale 1 (standard). Scale 2 (hypercapnic respiratory failure) is not implemented.
public static int ScoreSpo2(decimal value) => value switch
{
<= 91 => 3,
<= 93 => 2,
<= 95 => 1,
_ => 0 // >= 96
};
public static int ScoreSystolicBp(decimal value) => value switch
{
<= 90 => 3,
<= 100 => 2,
<= 110 => 1,
<= 219 => 0,
_ => 3 // >= 220
};
public static int ScoreHeartRate(decimal value) => value switch
{
<= 40 => 3,
<= 50 => 1,
<= 90 => 0,
<= 110 => 1,
<= 130 => 2,
_ => 3 // >= 131
};
// AVPU: Alert=0, Voice/Pain/Unresponsive=3 (any non-Alert scores 3)
public static int ScoreConsciousness(decimal value) => value switch
{
0 => 0, // Alert
_ => 3 // Voice (1), Pain (2), Unresponsive (3)
};
public static int ScoreTemperature(decimal value) => value switch
{
<= 35.0m => 3,
<= 36.0m => 1,
<= 38.0m => 0,
<= 39.0m => 1,
_ => 2 // >= 39.1
};
// 0 = room air, 1 = on supplemental oxygen
public static int ScoreSupplementalO2(decimal value) =>
value >= 1 ? 2 : 0;
// Dispatch to the correct scoring function by observation code.
public static int ScoreParameter(string observationCode, decimal value) =>
observationCode switch
{
"RESP_RATE" => ScoreRespRate(value),
"SPO2" => ScoreSpo2(value),
"SYSTOLIC_BP" => ScoreSystolicBp(value),
"HEART_RATE" => ScoreHeartRate(value),
"AVPU" => ScoreConsciousness(value),
"TEMP_C" => ScoreTemperature(value),
"SUPPLEMENTAL_O2" => ScoreSupplementalO2(value),
_ => throw new ArgumentOutOfRangeException(nameof(observationCode))
};
// Determine risk level from total score and single-param-3 flag.
public static string DetermineRiskLevel(int totalScore, bool hasSingleParamThree) =>
totalScore switch
{
>= 7 => "HIGH",
>= 5 => "MEDIUM",
_ when hasSingleParamThree => "LOW_MEDIUM",
_ => "LOW"
};
}