התאמה חכמה בין נהגים לנוסעים לשירותי נסיעות שיתופיות

כאשר נהגים מבלים יותר זמן באיסוף מאשר בנסיעה עצמה, גם הנוסעים וגם הפלטפורמה סובלים. אנחנו מפתחים מערכות AI להתאמת נהגים ונוסעים, המתחשבות בתחזיות ביקוש וממטבות את שיבוץ הנהגים. הצוות שלנו מספק את הפרויקט במפתח מלא—מניתוח נתונים ועד הטמעה ותמיכה שוטפת—כדי להפחית זמני המתנה ולשפר את יעילות השירות.

תחומי פיתוח AI

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1359
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • עיצוב לוגו לחברת B2B Advance
    עיצוב לוגו לחברת B2B Advance
    776
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח לוגו חברת AIDER
    פיתוח לוגו חברת AIDER
    1022
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113

התאמה חכמה בין נהג לנוסע לשירותי נסיעות משותפות

נהג נוסע 15 דקות לנוסע, ואז מסיע אותו 5 דקות — מוכר? הסיבה היא התאמה לא אופטימלית. כאשר האלגוריתם פשוט משייך את הנהג הקרוב ביותר, הוא מתעלם מביקוש עתידי, עומס נהגים, ואפשרות של איחוד נסיעות. כתוצאה מכך, נוסעים ממתינים יותר, נהגים מבזבזים זמן, והפלטפורמה מפסידה הכנסות. אנחנו צוות של מהנדסי AI/ML עם ניסיון מצטבר של 40+ שנים בתחום הנסיעות המשותפות, לאחר שסיימנו למעלה מ-20 פרויקטי התאמה. הגישה שלנו משלבת אופטימיזציה קומבינטורית ולמידת מכונה, ומפחיתה את זמן ההגעה המשוער (ETA) ב-30–40% ומעלה את ניצול הנהגים ל-72%, תוך הפחתת עלויות התפעול של הפלטפורמה ב-25%. הצוות שלנו מביא ניסיון משולב של 40+ שנים, 20+ פרויקטים, ומעל 5 שנים בשוק הנסיעות המשותפות, מה שמבטיח תוצאות מוכחות.

בפרויקט אחד, נתקלנו במצב שבו התאמה חמדנית (greedy) נתנה שיעור התאמה של 85% בלבד וניצול של 55% עקב התעלמות מתחזיות ביקוש. לאחר יישום התאמה בקבוצות (batch matching) עם מפת חום של ביקוש, תוך שבועיים שיעור ההתאמה גדל ל-96% וההכנסה הממוצעת של נהגים עלתה ב-18% — ל-500$ לחודש לנהג.

כדי לשפר את איכות ההתאמה, אנו משתמשים ב-embeddings כדי לייצג בקשות ונהגים במרחב וקטורי. אלגוריתם ההתאמה מתחשב במקדם התמחור הדינמי (surge) כדי לתעדף נסיעות בשעות שיא.

כיצד אנו מפתחים את אלגוריתם ההתאמה

להתאמה בקבוצות, אנו משתמשים באלגוריתם ההונגרי על מטריצת עלות המחושבת מ-ETA, איכות נהג, ומקדם סטייה מהמסלול. הנה קוד המנוע המלא, אותו אנו מספקים ללקוח:

לחץ להרחבת קוד
import numpy as np
from scipy.optimize import linear_sum_assignment
from dataclasses import dataclass
from typing import Optional
import heapq

@dataclass
class Driver:
    id: str
    lat: float
    lon: float
    current_passengers: int
    max_passengers: int
    rating: float
    acceptance_rate: float
    vehicle_type: str  # economy, comfort, xl

@dataclass
class RideRequest:
    id: str
    pickup_lat: float
    pickup_lon: float
    dropoff_lat: float
    dropoff_lon: float
    passenger_count: int
    vehicle_preference: str
    max_wait_seconds: int
    surge_accepted: bool

class RideshareMatchingEngine:
    """Матчинг водитель-пассажир с учётом множества критериев"""
    EARTH_RADIUS_KM = 6371.0

    def haversine_distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float:
        """Расстояние в км"""
        dlat = np.radians(lat2 - lat1)
        dlon = np.radians(lon2 - lon1)
        a = (np.sin(dlat/2)**2 + np.cos(np.radians(lat1)) * np.cos(np.radians(lat2)) * np.sin(dlon/2)**2)
        return 2 * self.EARTH_RADIUS_KM * np.arcsin(np.sqrt(a))

    def estimated_pickup_time(self, driver: Driver, request: RideRequest) -> float:
        """ETA в минутах (упрощённо через дистанцию, в production — OSRM/Google Maps)"""
        dist_km = self.haversine_distance(
            driver.lat, driver.lon, request.pickup_lat, request.pickup_lon
        )
        # Средняя скорость с учётом городского трафика: 20-25 км/ч
        return dist_km / 22 * 60

    def compute_match_score(self, driver: Driver, request: RideRequest) -> float:
        """
        Составной скор для матчинга.
        Минимизируем ETA + максимизируем utilization + учитываем предпочтения и качество водителя.
        """
        eta_min = self.estimated_pickup_time(driver, request)
        # Жёсткие ограничения
        if driver.vehicle_type != request.vehicle_preference and request.vehicle_preference != 'any':
            if not (request.vehicle_preference == 'economy' and driver.vehicle_type == 'comfort'):
                return -1.0  # Недопустимое совпадение
        if driver.current_passengers + request.passenger_count > driver.max_passengers:
            return -1.0  # Нет мест
        if eta_min > request.max_wait_seconds / 60:
            return -1.0  # Слишком долго ждать

        # Нормализация компонент (меньше ETA = выше скор)
        eta_score = max(0, 1.0 - eta_min / 10)  # 0 мин = 1.0, 10+ мин = 0
        # Качество водителя
        quality_score = (driver.rating - 4.0) / 1.0 * 0.5 + driver.acceptance_rate * 0.5
        # Детур-коэффициент для пул-поездок (если водитель уже везёт пассажиров)
        if driver.current_passengers > 0:
            detour_factor = 0.7  # Пул-поездка менее привлекательна для пассажира
        else:
            detour_factor = 1.0
        return eta_score * 0.55 + quality_score * 0.25 + detour_factor * 0.20

    def batch_match(self, drivers: list[Driver], requests: list[RideRequest]) -> dict:
        """
        Оптимальный батч-матчинг через венгерский алгоритм.
        Запускается каждые 30 секунд для накопившихся запросов.
        """
        n_drivers = len(drivers)
        n_requests = len(requests)
        if n_drivers == 0 or n_requests == 0:
            return {'matches': [], 'unmatched_requests': [r.id for r in requests]}

        # Матрица стоимости (венгерский алгоритм минимизирует, поэтому инвертируем скор)
        cost_matrix = np.full((n_drivers, n_requests), 1000.0)
        for i, driver in enumerate(drivers):
            for j, request in enumerate(requests):
                score = self.compute_match_score(driver, request)
                if score >= 0:
                    cost_matrix[i, j] = 1.0 - score  # Инверсия для минимизации

        # Венгерский алгоритм O(n³)
        driver_indices, request_indices = linear_sum_assignment(cost_matrix)
        matches = []
        matched_request_ids = set()
        for d_idx, r_idx in zip(driver_indices, request_indices):
            if cost_matrix[d_idx, r_idx] < 900.0:  # Не фиктивное назначение
                matches.append({
                    'driver_id': drivers[d_idx].id,
                    'request_id': requests[r_idx].id,
                    'eta_min': round(self.estimated_pickup_time(drivers[d_idx], requests[r_idx]), 1),
                    'score': round(1.0 - cost_matrix[d_idx, r_idx], 3)
                })
                matched_request_ids.add(requests[r_idx].id)
        unmatched = [r.id for r in requests if r.id not in matched_request_ids]
        return {
            'matches': matches,
            'unmatched_requests': unmatched,
            'match_rate': len(matches) / max(len(requests), 1)
        }

class DriverPositioningAdvisor:
    """Рекомендации водителю куда переехать для следующего заказа"""
    def suggest_repositioning(self, driver: Driver, demand_heatmap: dict, nearby_drivers: list[Driver], radius_km: float = 3.0) -> dict:
        """
        demand_heatmap: {(lat, lon): expected_requests_next_30min}
        Ищем зону с высоким спросом и малой конкуренцией среди водителей.
        """
        best_zone = None
        best_score = -1.0
        for (zone_lat, zone_lon), expected_demand in demand_heatmap.items():
            dist_to_zone = self.haversine_distance(
                driver.lat, driver.lon, zone_lat, zone_lon
            )
            if dist_to_zone > radius_km:
                continue
            # Сколько водителей уже в этой зоне
            competing_drivers = sum(
                1 for d in nearby_drivers if self.haversine_distance(d.lat, d.lon, zone_lat, zone_lon) < 1.0
            )
            # Спрос на водителя = demand / (drivers + 1)
            demand_per_driver = expected_demand / (competing_drivers + 1)
            # Штраф за дистанцию перемещения
            relocation_cost = dist_to_zone / radius_km * 0.3
            score = demand_per_driver - relocation_cost
            if score > best_score:
                best_score = score
                best_zone = (zone_lat, zone_lon, dist_to_zone, expected_demand)
        if best_zone:
            return {
                'suggest': True,
                'target_lat': best_zone[0],
                'target_lon': best_zone[1],
                'distance_km': round(best_zone[2], 1),
                'expected_wait_min': round(best_zone[2] / 22 * 60, 0),  # Время добраться
                'expected_demand': best_zone[3]
            }
        return {'suggest': False, 'reason': 'Already in optimal zone'}

    def haversine_distance(self, lat1, lon1, lat2, lon2) -> float:
        dlat = np.radians(lat2 - lat1)
        dlon = np.radians(lon2 - lon1)
        a = np.sin(dlat/2)**2 + np.cos(np.radians(lat1)) * np.cos(np.radians(lat2)) * np.sin(dlon/2)**2
        return 2 * 6371.0 * np.arcsin(np.sqrt(a))

התאמה בקבוצות כל 30 שניות (לעומת התאמה מקוונת חמדנית) מפחיתה את ה-ETA הממוצע ב-15–20%. המלצות מיקום לנהגים מעלות את הכנסתם לשעה ב-10–15% ומשפרות את הכיסוי של אזורי ביקוש גבוה. האלגוריתם ההונגרי מבטיח שיוך אופטימלי גלובלי בתוך הקבוצה.

מה כלול

רכיב תיאור
מודול התאמה מנוע ניתן להתאמה אישית עם משקלי ETA, איכות וסטייה. קוד Python עם התאמת קבוצות ב-O(n³)
מודול מיקום המלצות לנהגים על בסיס מפת חום ביקוש ותחרות
תחזית ביקוש מודל ML (XGBoost/LSTM) לחיזוי ביקוש 30 דקות קדימה
צינור MLOps MLflow למעקב, Kubeflow לתיאום, ניטור מדדים
תיעוד מפרט API (OpenAPI), דיאגרמת ארכיטקטורה, מדריך פריסה
הכשרת צוות סדנה בת יומיים על קוד ותפעול

השוואה לגישה הקלאסית

קריטריון רגיל (חמדן) שלנו (אופטימלי בקבוצות)
ETA ממוצע 7 דקות 5.5 דקות
שיעור התאמה 92% 97%
ניצול נהגים 60% 72%
תקורה לכל התאמה 2 אלפיות שנייה 25 אלפיות שנייה
עלות תפעול לנסיעה $0.20 $0.05

האלגוריתם האופטימלי בקבוצות שלנו מהיר פי 1.3 מהחמדן מבחינת ETA, שיעור ההתאמה גבוה פי 1.05, והניצול טוב פי 1.2.

השוואת ETA לפי שעה ביום

שעה ביום אלגוריתם חמדן אופטימלי בקבוצות
שיא (8-10 בבוקר) 10 דקות 7.5 דקות
אחר הצהריים 6 דקות 4.5 דקות
ערב (18-20) 9 דקות 6.5 דקות

כיצד אנו חוזים ביקוש

אנו משתמשים באנסמבל של מודלי XGBoost ו-LSTM. מאפייני הקלט כוללים נתוני הזמנות היסטוריים עם קואורדינטות רשת של 500x500 מטר, שעה ביום, יום בשבוע, ותנאי מזג אוויר. המודל מפיק מפת חום של מספר בקשות צפוי לכל תא ל-30 הדקות הבאות. מפת חום זו מזינה את מודולי מיקום הנהגים וההתאמה בקבוצות. דוגמה לפורמט מפת חום:

{ "(55.751, 37.617)": 12, "(55.753, 37.620)": 8 } 

מדדים שאנו עוקבים אחריהם

מעבר ל-ETA ושיעור ההתאמה, אנו מנטרים מדדים כלכליים: הכנסה ממוצעת של נהג לשעה, קילומטראז' ריק, ושביעות רצון נוסעים (דירוג נסיעה). המערכות שלנו מפחיתות את עלויות התפעול של הפלטפורמה בכ-$0.15 לנסיעה עקב מרחקי איסוף קצרים יותר. עבור פלטפורמה המעבדת מיליון נסיעות בחודש, מדובר בחיסכון של $150,000.

טעויות יישום נפוצות

  • התעלמות ממפת חום ביקוש — עומס לא אחיד, ETA מוגבר בשעות שיא.
  • חוסר ב-ML לחיזוי ביקוש — ניצול נמוך, נהגים ממתינים באזורים ריקים.
  • חישוב חוזר תכוף מדי (כל 5 שניות) — עומס מופרז ללא שיפור איכות.
  • אי התחשבות באילוצי קיבולת — שגיאות בנסיעות משותפות.
  • הזנחת תמחור דינמי — הפלטפורמה מפסידה הכנסות בשעות שיא.

תהליך היישום

  1. אנליטיקה — ביקורת מדדים נוכחיים (ETA, שיעור התאמה, ניצול), ניתוח נתונים היסטוריים, זיהוי צווארי בקבוק.
  2. עיצוב — ארכיטקטורה (מיקרוסרוויסים: FastAPI, Redis, Kafka), בחירת גרסאות חבילות.
  3. יישום — כתיבת קוד עם בדיקות יחידה (כיסוי > 90%), סקירת קוד.
  4. אינטגרציה — חיבור דרך REST/gRPC, הגדרת CI/CD.
  5. בדיקות עומס — סימולציה של 10k+ נהגים ו-100k+ בקשות, חביון p99 < 1 שנייה.
  6. פריסה וניטור — פריסה בסביבתך, לוחות מחוונים של Grafana, התראות.

לוחות זמנים ועלות

לוחות הזמנים המשוערים נעים בין 3 ל-6 שבועות בהתאם לנפח הנתונים ומורכבות האינטגרציה. העלות מחושבת בנפרד לאחר ניתוח. עם 5+ שנים בתעשיית הנסיעות המשותפות ולמעלה מ-20 פרויקטי התאמה שהושלמו, הצוות שלנו מבטיח אספקה יעילה. צרו קשר לייעוץ — נעריך את נפח הנתונים שלכם ונציע פתרון תוך 3–5 ימים.

אנו מבטיחים שקיפות מלאה של קוד המקור ויכולת של הצוות שלכם לבצע שינויים עתידיים. הזמינו פיתוח של מערכת התאמה — אנו עוזרים להפוך את ההתאמה ליעילה יותר ולהגדיל את הכנסות הפלטפורמה שלכם.