187 lines
7.5 KiB
Markdown
187 lines
7.5 KiB
Markdown
# 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<cluster>
|
|
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)
|
|
```
|