scoring.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. from __future__ import annotations
  2. from dataclasses import dataclass
  3. from app.market.models import CandidateRecord, InquiryRequest
  4. DECISION_WEIGHTS = {
  5. "solution_assurance": 0.27,
  6. "relationship_advantage": 0.23,
  7. "spot_space": 0.15,
  8. "supplier_execution": 0.18,
  9. "customer_fit": 0.10,
  10. "transit": 0.04,
  11. "price": 0.03,
  12. }
  13. SPOT_SPACE_STATUS_SCORES = {
  14. "可立即订舱": 100.0,
  15. "现舱有限": 70.0,
  16. "需确认": 35.0,
  17. "无现舱": 0.0,
  18. }
  19. @dataclass(frozen=True)
  20. class CustomerSignals:
  21. price_sensitivity: float
  22. time_sensitivity: float
  23. reliability_sensitivity: float
  24. repeat_purchase_count: int
  25. overdue_days: int
  26. service_requirement: str
  27. def score_candidates(
  28. candidates: list[CandidateRecord],
  29. inquiry: InquiryRequest,
  30. customer: CustomerSignals,
  31. ) -> tuple[list[dict], dict[str, float]]:
  32. if not candidates:
  33. return [], {}
  34. costs = [candidate.supplier_cost for candidate in candidates]
  35. min_cost, max_cost = min(costs), max(costs)
  36. min_transit = min(candidate.transit_days for candidate in candidates)
  37. scored: list[dict] = []
  38. for candidate in candidates:
  39. if max_cost == min_cost:
  40. price_score = 100.0
  41. else:
  42. price_score = 100 - ((candidate.supplier_cost - min_cost) / (max_cost - min_cost) * 28)
  43. transit_penalty = 12 if candidate.transport_mode == "空运" else 5
  44. transit_score = max(
  45. 40.0,
  46. 100 - (candidate.transit_days - min_transit) * transit_penalty,
  47. )
  48. offering_reliability = (
  49. candidate.booking_success_rate + candidate.space_release_success_rate
  50. ) / 2
  51. product_reliability = (
  52. candidate.etd_on_time_rate + candidate.eta_on_time_rate
  53. ) / 2
  54. supplier_score = (offering_reliability * 0.7 + product_reliability * 0.3) * 100
  55. route_assurance = 100.0 if candidate.route_type in {"直航", "直达"} else 72.0
  56. solution_score = (
  57. candidate.solution_assurance * 100 * 0.55
  58. + route_assurance * 0.25
  59. + candidate.service_support_score * 100 * 0.20
  60. )
  61. relationship_score = (
  62. candidate.relationship_strength * 0.55
  63. + candidate.low_price_access_probability * 0.25
  64. + candidate.priority_space_probability * 0.20
  65. ) * 100
  66. spot_space_score = (
  67. SPOT_SPACE_STATUS_SCORES.get(candidate.spot_space_status, 35.0) * 0.85
  68. + min(candidate.spot_space_teu / 16, 1.0) * 100 * 0.15
  69. )
  70. customer_score = 55.0
  71. if candidate.route_type == inquiry.preferred_route_type:
  72. customer_score += 15
  73. customer_score += candidate.service_support_score * customer.reliability_sensitivity * 15
  74. customer_score += (transit_score / 100) * customer.time_sensitivity * 10
  75. customer_score += (price_score / 100) * customer.price_sensitivity * 5
  76. customer_score = min(100.0, customer_score)
  77. total_score = (
  78. solution_score * DECISION_WEIGHTS["solution_assurance"]
  79. + relationship_score * DECISION_WEIGHTS["relationship_advantage"]
  80. + spot_space_score * DECISION_WEIGHTS["spot_space"]
  81. + supplier_score * DECISION_WEIGHTS["supplier_execution"]
  82. + customer_score * DECISION_WEIGHTS["customer_fit"]
  83. + transit_score * DECISION_WEIGHTS["transit"]
  84. + price_score * DECISION_WEIGHTS["price"]
  85. )
  86. scored.append({
  87. "candidate": candidate,
  88. "price_score": round(price_score, 1),
  89. "transit_score": round(transit_score, 1),
  90. "supplier_execution_score": round(supplier_score, 1),
  91. "customer_fit_score": round(customer_score, 1),
  92. "solution_assurance_score": round(solution_score, 1),
  93. "relationship_advantage_score": round(relationship_score, 1),
  94. "spot_space_score": round(spot_space_score, 1),
  95. "total_score": round(total_score, 1),
  96. })
  97. assign_solution_types(scored)
  98. scored.sort(key=lambda item: item["total_score"], reverse=True)
  99. for rank, item in enumerate(scored, start=1):
  100. item["rank"] = rank
  101. return scored, DECISION_WEIGHTS.copy()
  102. def assign_solution_types(scored: list[dict]) -> None:
  103. for item in scored:
  104. item["solution_type"] = "可执行方案"
  105. primary = max(scored, key=lambda item: item["total_score"])
  106. primary["solution_type"] = "方案优选"
  107. remaining = [item for item in scored if item is not primary]
  108. if remaining:
  109. relationship = max(remaining, key=lambda item: item["relationship_advantage_score"])
  110. relationship["solution_type"] = "关系优选"
  111. remaining = [item for item in remaining if item is not relationship]
  112. if remaining:
  113. price = min(remaining, key=lambda item: item["candidate"].supplier_cost)
  114. price["solution_type"] = "价格参考"
  115. remaining = [item for item in remaining if item is not price]
  116. if remaining:
  117. fastest = min(remaining, key=lambda item: item["candidate"].transit_days)
  118. fastest["solution_type"] = "时效参考"