SmartOpsImprovementEffectiveness.cs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. namespace Admin.NET.Plugin.AiDOP.SmartOps;
  2. public static class SmartOpsImprovementEffectiveness
  3. {
  4. public const int WindowSize = 7;
  5. public const int MinSamples = 3;
  6. public const string AutoEffective = "AUTO_EFFECTIVE";
  7. public const string AutoIneffective = "AUTO_INEFFECTIVE";
  8. public const string AutoInconclusive = "AUTO_INCONCLUSIVE";
  9. public const string InsufficientData = "INSUFFICIENT_DATA";
  10. public static bool IsLowerBetter(string? direction) =>
  11. string.Equals(direction?.Trim(), "lower_is_better", StringComparison.OrdinalIgnoreCase);
  12. public static IReadOnlyList<decimal> TakeLastValid(IEnumerable<decimal?> values, int take = WindowSize)
  13. {
  14. return values.Where(x => x.HasValue).Select(x => x!.Value).TakeLast(take).ToList();
  15. }
  16. public static bool MeetsTarget(decimal value, decimal? target, string? direction)
  17. {
  18. if (target is null) return false;
  19. return IsLowerBetter(direction) ? value <= target.Value : value >= target.Value;
  20. }
  21. public static int TrailingOnTargetDays(IReadOnlyList<decimal> after, decimal? target, string? direction)
  22. {
  23. if (target is null || after.Count == 0) return 0;
  24. var count = 0;
  25. for (var i = after.Count - 1; i >= 0; i--)
  26. {
  27. if (!MeetsTarget(after[i], target, direction)) break;
  28. count++;
  29. }
  30. return count;
  31. }
  32. public static SmartOpsEffectivenessResult Evaluate(
  33. IReadOnlyList<decimal> before,
  34. IReadOnlyList<decimal> after,
  35. decimal? target,
  36. string? direction,
  37. int exceptionCountBefore = 0,
  38. int exceptionCountAfter = 0)
  39. {
  40. if (before.Count < MinSamples || after.Count < MinSamples)
  41. {
  42. return new SmartOpsEffectivenessResult(
  43. InsufficientData,
  44. Mean(before),
  45. Mean(after),
  46. null,
  47. before.Count,
  48. after.Count,
  49. TrailingOnTargetDays(after, target, direction),
  50. AchievementRate(after, target, direction),
  51. exceptionCountBefore,
  52. exceptionCountAfter,
  53. "有效样本不足 3 个,无法自动评价");
  54. }
  55. var beforeMean = Mean(before)!.Value;
  56. var afterMean = Mean(after)!.Value;
  57. var lower = IsLowerBetter(direction);
  58. var meanImproved = lower ? afterMean < beforeMean : afterMean > beforeMean;
  59. var meanWorse = lower ? afterMean > beforeMean : afterMean < beforeMean;
  60. var delta = afterMean - beforeMean;
  61. var reached = target is not null && MeetsTarget(afterMean, target, direction);
  62. var closer = target is not null
  63. && Math.Abs(afterMean - target.Value) < Math.Abs(beforeMean - target.Value);
  64. string conclusion;
  65. string reason;
  66. if (meanImproved && (reached || closer))
  67. {
  68. conclusion = AutoEffective;
  69. reason = reached ? "后窗口均值改善且已达目标" : "后窗口均值改善且更接近目标";
  70. }
  71. else if (meanWorse)
  72. {
  73. conclusion = AutoIneffective;
  74. reason = "后窗口均值劣于改善前,未判定为有效";
  75. }
  76. else
  77. {
  78. conclusion = AutoInconclusive;
  79. reason = target is null
  80. ? "缺少目标值,不能只按差值正负判定"
  81. : meanImproved
  82. ? "均值改善但未更接近或达到目标"
  83. : "前后窗口均值无显著方向变化";
  84. }
  85. return new SmartOpsEffectivenessResult(
  86. conclusion,
  87. beforeMean,
  88. afterMean,
  89. delta,
  90. before.Count,
  91. after.Count,
  92. TrailingOnTargetDays(after, target, direction),
  93. AchievementRate(after, target, direction),
  94. exceptionCountBefore,
  95. exceptionCountAfter,
  96. reason);
  97. }
  98. private static decimal? Mean(IReadOnlyList<decimal> values) =>
  99. values.Count == 0 ? null : decimal.Round(values.Average(), 4);
  100. private static decimal AchievementRate(IReadOnlyList<decimal> after, decimal? target, string? direction)
  101. {
  102. if (target is null || after.Count == 0) return 0;
  103. var hit = after.Count(v => MeetsTarget(v, target, direction));
  104. return decimal.Round(hit * 100m / after.Count, 2);
  105. }
  106. }
  107. public sealed record SmartOpsEffectivenessResult(
  108. string Conclusion,
  109. decimal? BeforeMean,
  110. decimal? AfterMean,
  111. decimal? Delta,
  112. int BeforeSamples,
  113. int AfterSamples,
  114. int ConsecutiveOnTargetDays,
  115. decimal AchievementRate,
  116. int ExceptionCountBefore,
  117. int ExceptionCountAfter,
  118. string Reason);