scoring.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. from __future__ import annotations
  2. from dataclasses import dataclass
  3. from app.market.models import CandidateRecord, InquiryRequest
  4. @dataclass(frozen=True)
  5. class CustomerSignals:
  6. price_sensitivity: float
  7. time_sensitivity: float
  8. reliability_sensitivity: float
  9. repeat_purchase_count: int
  10. overdue_days: int
  11. service_requirement: str
  12. def score_candidates(
  13. candidates: list[CandidateRecord],
  14. inquiry: InquiryRequest,
  15. customer: CustomerSignals,
  16. ) -> tuple[list[dict], dict[str, float]]:
  17. if not candidates:
  18. return [], {}
  19. costs = [candidate.supplier_cost for candidate in candidates]
  20. min_cost, max_cost = min(costs), max(costs)
  21. min_transit = min(candidate.transit_days for candidate in candidates)
  22. raw_weights = {
  23. "price": 1 + customer.price_sensitivity,
  24. "transit": 0.8 + customer.time_sensitivity,
  25. "supplier": 1 + customer.reliability_sensitivity,
  26. "customer": 0.9,
  27. "strategy": 0.5,
  28. }
  29. weight_total = sum(raw_weights.values())
  30. weights = {name: value / weight_total for name, value in raw_weights.items()}
  31. scored: list[dict] = []
  32. for candidate in candidates:
  33. if max_cost == min_cost:
  34. price_score = 100.0
  35. else:
  36. price_score = 100 - ((candidate.supplier_cost - min_cost) / (max_cost - min_cost) * 35)
  37. transit_penalty = 12 if candidate.transport_mode == "空运" else 5
  38. transit_score = max(
  39. 40.0,
  40. 100 - (candidate.transit_days - min_transit) * transit_penalty,
  41. )
  42. offering_reliability = (
  43. candidate.booking_success_rate + candidate.space_release_success_rate
  44. ) / 2
  45. product_reliability = (
  46. candidate.etd_on_time_rate + candidate.eta_on_time_rate
  47. ) / 2
  48. supplier_score = (offering_reliability * 0.7 + product_reliability * 0.3) * 100
  49. is_brand_unit = "品牌本地分支机构" in candidate.agency_level
  50. customer_score = 94.0 if is_brand_unit else 83.0
  51. if candidate.route_type == inquiry.preferred_route_type:
  52. customer_score += 4
  53. if customer.time_sensitivity >= 0.85:
  54. customer_score += 8
  55. if customer.price_sensitivity >= 0.7 and not is_brand_unit:
  56. customer_score += 7
  57. if customer.reliability_sensitivity >= 0.85 and is_brand_unit:
  58. customer_score += 2
  59. customer_score = min(100.0, customer_score)
  60. strategy_score = 92.0 if candidate.route_type == "直航" else 72.0
  61. total_score = (
  62. price_score * weights["price"]
  63. + transit_score * weights["transit"]
  64. + supplier_score * weights["supplier"]
  65. + customer_score * weights["customer"]
  66. + strategy_score * weights["strategy"]
  67. )
  68. scored.append({
  69. "candidate": candidate,
  70. "price_score": round(price_score, 1),
  71. "transit_score": round(transit_score, 1),
  72. "supplier_execution_score": round(supplier_score, 1),
  73. "customer_fit_score": round(customer_score, 1),
  74. "product_strategy_score": round(strategy_score, 1),
  75. "total_score": round(total_score, 1),
  76. })
  77. assign_solution_types(scored)
  78. scored.sort(key=lambda item: item["total_score"], reverse=True)
  79. for rank, item in enumerate(scored, start=1):
  80. item["rank"] = rank
  81. return scored, {name: round(value, 4) for name, value in weights.items()}
  82. def assign_solution_types(scored: list[dict]) -> None:
  83. economy = min(scored, key=lambda item: item["candidate"].supplier_cost)
  84. fastest = min(
  85. (item for item in scored if item is not economy),
  86. key=lambda item: item["candidate"].transit_days,
  87. default=economy,
  88. )
  89. balanced = max(
  90. (item for item in scored if item not in (economy, fastest)),
  91. key=lambda item: item["total_score"],
  92. default=max(scored, key=lambda item: item["total_score"]),
  93. )
  94. for item in scored:
  95. item["solution_type"] = "候选方案"
  96. economy["solution_type"] = "经济方案"
  97. fastest["solution_type"] = "时效方案"
  98. balanced["solution_type"] = "均衡方案"