# Fahrer Home / NotHome Klassifikation + Trip-Segmentierung — Business Rules ``` # ════════════════════════════════════════════════════════════ # Quelle: Tachograph M_ (Vehicle Unit, GNSS-Positionen) # + C_ (Fahrerkarte, Aktivitäten / Steckung+Entnahme) # ════════════════════════════════════════════════════════════ # ───────────────────── Konstanten ───────────────────── CONST NDI_LONG = 7.5h # Schwelle "langes" NDI CONST NDI_VERY_LONG = 24h CONST CARD_REMOVAL_PCT = 0.80 # 80% der NDI-Dauer CONST VISIT_SHARE = 0.25 # 25% aller (NDI > 7.5h) CONST DBSCAN_EPS = 150m # geo-Metrik (Haversine / PostGIS) CONST DBSCAN_MIN_PTS = 3 ENUM HomeStatus { HOME, NOT_HOME } # ───────────────────── Datenmodell ───────────────────── STRUCT DI: # Lenkintervall driverId vehicleId start; end posStart; posEnd # (x,y) aus M_, nullable gnssTrace[] # (ts, x, y) Stützpunkte aus M_ STRUCT NDI: # Nicht-Lenkintervall (Ruhe) driverId vehicleStart; vehicleEnd # Fahrzeug der vorigen / nächsten DI start; end # = vorige.end / nächste.start pos = null # zugeordnete (x,y), nullable cardOut = null # Intervall [Entnahme, Steckung], nullable cluster = null # DBSCAN-Cluster-Id (inkl. NOISE) status = null # HOME | NOT_HOME FUNCTION dur(iv) = iv.end - iv.start # ═══════════ 1. NDI aus aufeinanderfolgenden DI ableiten ═══════════ FUNCTION buildNDIs(driverId, dis): # dis chronologisch sortiert out = [] FOR i IN 1 .. len(dis)-1: prev = dis[i-1]; next = dis[i] out += NDI { driverId : driverId vehicleStart : prev.vehicleId vehicleEnd : next.vehicleId start : prev.end end : next.start pos : assignPos(prev, next) cardOut : cardRemovalInterval(prev.end, next.start) # aus C_ Events } RETURN out # Fahrzeug steht während NDI -> letzte bekannte Position zuordnen FUNCTION assignPos(prev, next): RETURN prev.posEnd ?? next.posStart # nullable # ═══════════ 2. LocationStatistik + DBSCAN ═══════════ # nur NDI > 7.5h mit bekannter Position fließen in die Statistik FUNCTION clusterLongNDIs(allNDIs): longs = [n IN allNDIs WHERE dur(n) > NDI_LONG AND n.pos != null] labels = DBSCAN(points = [n.pos FOR n IN longs], eps = DBSCAN_EPS, minPts = DBSCAN_MIN_PTS, metric = HAVERSINE) FOR n, lbl IN zip(longs, labels): n.cluster = lbl RETURN longs # inkl. NOISE-Label # ═══════════ 3. Home-Locations bestimmen ═══════════ FUNCTION determineHomeLocations(longs): # 3a Firmen-Home (Depots): Cluster mit > 25% ALLER langen NDI totalCompany = count(longs) # Nenner = alle langen NDI companyHome = { c FOR (c, visits) IN groupByCluster(longs) WHERE c != NOISE AND count(visits) / totalCompany > VISIT_SHARE } # 3b Fahrer-Home (privat): pro Fahrer Cluster mit > 25% SEINER langen NDI, # außerhalb der Schnittmenge mit den Firmen-Clustern driverHome = {} # driverId -> Set FOR (driverId, dnNDIs) IN groupByDriver(longs): totalDriver = count(dnNDIs) FOR (c, visits) IN groupByCluster(dnNDIs): IF c != NOISE AND count(visits) / totalDriver > VISIT_SHARE AND c NOT IN companyHome: # Schnittmenge mit Firma entfernen driverHome[driverId] += c RETURN (companyHome, driverHome) # ═══════════ 4. Klassifikation Home / NotHome ═══════════ FUNCTION classify(n, companyHome, driverHome): # A Karte am NDI-Ende in anderem Fahrzeug gesteckt IF n.vehicleStart != n.vehicleEnd: RETURN HOME # B Karte > 80% des NDI entnommen IF n.cardOut != null AND dur(n.cardOut) > CARD_REMOVAL_PCT * dur(n): RETURN HOME # C Ruhe > 24h IF dur(n) > NDI_VERY_LONG: RETURN HOME # D keine Position bekannt IF n.pos == null: RETURN (dur(n) > NDI_LONG) ? HOME : NOT_HOME # E Position bekannt + langes NDI -> Entscheidung über Home-Location-Cluster IF dur(n) > NDI_LONG: IF n.cluster IN companyHome OR n.cluster IN driverHome[n.driverId]: RETURN HOME # Depot / privates Zuhause ELSE: RETURN NOT_HOME # Nächtigung im Fahrzeug # kurze Ruhe beim Fahrzeug RETURN NOT_HOME # ═══════════ 5. Grenzübertritte -> TripSegmente ═══════════ STRUCT TripSegment: driverId; vehicleId start; end countryFrom; countryTo posFrom; posTo # BorderCrossing = explizites Tacho-Event (Smart Tacho v2) # ODER Länderwechsel im reverse-geocodeten GNSS-Trace FUNCTION buildTripSegments(driverId, dis): # chronologisch segs = [] segStart = dis[0].start posFrom = dis[0].posStart country = countryOf(dis[0].posStart) # PostGIS / Nominatim FOR di IN dis: FOR p IN di.gnssTrace: # (ts, x, y) aus M_ c = countryOf(p) IF c != country: # -> Grenzübertritt segs += TripSegment { driverId : driverId vehicleId : di.vehicleId start : segStart end : p.ts countryFrom : country countryTo : c posFrom : posFrom posTo : p } segStart = p.ts; posFrom = p; country = c # Schluss-Segment (letzter Abschnitt ohne weiteren Übertritt) segs += TripSegment { driverId : driverId vehicleId : dis[last].vehicleId start : segStart end : dis[last].end countryFrom : country countryTo : country posFrom : posFrom posTo : dis[last].posEnd } RETURN segs # ═══════════ Orchestrierung ═══════════ FUNCTION run(files_M, files_C): acts = parseTacho(files_M, files_C) # DI + Karten-Events allNDIs = [] segments = [] FOR (driverId, dis) IN groupDIsByDriver(acts): dis = sortByTime(dis) allNDIs += buildNDIs(driverId, dis) segments += buildTripSegments(driverId, dis) # zwei Pässe: erst clustern, dann klassifizieren longs = clusterLongNDIs(allNDIs) (companyHome, driverHome) = determineHomeLocations(longs) FOR n IN allNDIs: n.status = classify(n, companyHome, driverHome) RETURN (allNDIs, segments) ```