buses.py

changeset 30
a5bfd99bc2a3
parent 29
2c78e68d7363
child 31
60045b362d71
equal deleted inserted replaced
29:2c78e68d7363 30:a5bfd99bc2a3
152 routes_per_id = {} 152 routes_per_id = {}
153 all_trips = {} 153 all_trips = {}
154 services = {} 154 services = {}
155 bus_stops = {} 155 bus_stops = {}
156 all_clusters = set() 156 all_clusters = set()
157 157 viimeinen_käyttöpäivä = None
158 print('Ladataan routes... ', file = stderr, end = '', flush = True) 158 clusters_by_name = {}
159 with open('gtfs/routes.txt') as file:
160 for row in read_csv(file):
161 route = BusRoute(row)
162 routes[route.reference] = route
163 routes_per_id[route.id] = route
164 print('%d linjaa' % len(routes), file = stderr)
165
166 print('Ladataan ajovuorot... ', file = stderr, end = '', flush = True)
167
168 shape_distances = {}
169 with open('gtfs/shapes.txt') as file:
170 for row in read_csv(file):
171 shape_distances[row['shape_id']] = max(shape_distances.get(row['shape_id'], 0), float(row['shape_dist_traveled']))
172
173 with open('gtfs/trips.txt') as file:
174 for row in read_csv(file):
175 if row['service_id'] not in services:
176 services[row['service_id']] = BusService(row['service_id'])
177 route = routes_per_id[row['route_id']]
178 trip = BusTrip(
179 reference = row['trip_id'],
180 route = route,
181 service = services[row['service_id']],
182 length = shape_distances[row['shape_id']]
183 )
184 route.trips.add(trip)
185 assert trip.name not in all_trips
186 all_trips[trip.name] = trip
187 print('%d ajoa' % len(all_trips), file = stderr)
188
189 def read_date(teksti):
190 return date(int(teksti[:4]), int(teksti[4:6]), int(teksti[6:]))
191
192 def read_time(teksti):
193 tunti, minuutti, sekunti = map(int, teksti.split(':'))
194 return timedelta(hours = tunti, minutes = minuutti, seconds = sekunti)
195
196 print('Ladataan päiväykset... ', file = stderr, flush = True)
197
198 viimeinen_käyttöpäivä = date.today()
199 services_for_day = {} 159 services_for_day = {}
200 160
201 def date_range(start_date, end_date, *, include_end = False): 161 def load_buses(gtfs_zip_path, profile):
202 ''' Generates date from start_date to end_date. If include_end is True, then end_date will be yielded. ''' 162 global viimeinen_käyttöpäivä
203 current_date = start_date 163 from zipfile import ZipFile
204 while current_date < end_date: 164 with ZipFile(gtfs_zip_path) as gtfs_zip:
205 yield current_date 165 print('Ladataan linjat... ', file = stderr, end = '', flush = True)
206 current_date += timedelta(1) 166 with gtfs_zip.open('routes.txt') as file:
207 if include_end: 167 for row in read_csv(map(bytes.decode, file)):
208 yield end_date 168 route = BusRoute(row)
209 169 routes[route.reference] = route
210 def add_day_to_service(service_name, day): 170 routes_per_id[route.id] = route
211 try: 171 print('%d linjaa' % len(routes), file = stderr)
212 service = services[service_name] 172
213 except KeyError: 173 print('Ladataan ajovuorot... ', file = stderr, end = '', flush = True)
214 return 174
215 else: 175 shape_distances = {}
216 service.dates.add(day) 176 with gtfs_zip.open('shapes.txt') as file:
217 if day not in services_for_day: 177 for row in read_csv(map(bytes.decode, file)):
218 services_for_day[day] = set() 178 shape_distances[row['shape_id']] = max(shape_distances.get(row['shape_id'], 0), float(row['shape_dist_traveled']))
219 services_for_day[day].add(service) 179
220 global viimeinen_käyttöpäivä 180 with gtfs_zip.open('trips.txt') as file:
221 viimeinen_käyttöpäivä = max(day, viimeinen_käyttöpäivä) 181 for row in read_csv(map(bytes.decode, file)):
222 182 if row['service_id'] not in services:
223 def filter_day(row, day): 183 services[row['service_id']] = BusService(row['service_id'])
224 day_names = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] 184 route = routes_per_id[row['route_id']]
225 return int(row[day_names[day.isoweekday() - 1]]) 185 trip = BusTrip(
226 186 reference = row['trip_id'],
227 with open('gtfs/calendar.txt') as file: 187 route = route,
228 for row in read_csv(file): 188 service = services[row['service_id']],
229 for day in date_range(read_date(row['start_date']), read_date(row['end_date']), include_end = True): 189 length = shape_distances[row['shape_id']] * float(profile['metrics']['shape-modifier'])
230 if filter_day(row, day):
231 add_day_to_service(service_name = row['service_id'], day = day)
232
233 with open('gtfs/calendar_dates.txt') as file:
234 for row in read_csv(file):
235 add_day_to_service(service_name = row['service_id'], day = read_date(row['date']))
236
237 def services_available_at(day):
238 for service in services.values():
239 if day in service.dates:
240 yield service
241
242 print('Ladataan pysäkit... ', file = stderr, end = '', flush = True)
243 with open('gtfs/stops.txt') as file:
244 for row in read_csv(file):
245 location = Sijainti(float(row['stop_lat']), float(row['stop_lon']))
246 stop = BusStop(
247 reference = row['stop_id'],
248 name = row['stop_name'],
249 location = location,
250 code = row['stop_code'],
251 )
252 bus_stops[stop.reference] = stop
253 with open('regions-per-stop.json') as file:
254 for stop_reference, region in json.load(file).items():
255 bus_stops[stop_reference].region = region
256 print('%d pysäkkiä' % len(bus_stops), file = stderr)
257
258
259 class BusStopCluster:
260 def __init__(self):
261 self.stops = set()
262 self._center = None
263 self.name = None
264 @property
265 def url_name(self):
266 return self.name.lower().replace('(', '').replace(')', '').replace(' ', '-')
267 def add_stop(self, stop):
268 assert not stop.cluster
269 stop.cluster = self
270 self.stops.add(stop)
271 self._center = None
272 @property
273 def center(self):
274 if not self._center:
275 if self.stops:
276 from statistics import median
277 pointtype = type(next(iter(self.stops)).location)
278 self._center = pointtype(
279 median(stop.location.x for stop in self.stops),
280 median(stop.location.y for stop in self.stops),
281 ) 190 )
191 route.trips.add(trip)
192 assert trip.name not in all_trips
193 all_trips[trip.name] = trip
194 print('%d ajoa' % len(all_trips), file = stderr)
195
196 def read_date(teksti):
197 return date(int(teksti[:4]), int(teksti[4:6]), int(teksti[6:]))
198
199 def read_time(teksti):
200 tunti, minuutti, sekunti = map(int, teksti.split(':'))
201 return timedelta(hours = tunti, minutes = minuutti, seconds = sekunti)
202
203 print('Ladataan päiväykset... ', file = stderr, flush = True)
204
205 viimeinen_käyttöpäivä = date.today()
206
207 def date_range(start_date, end_date, *, include_end = False):
208 ''' Generates date from start_date to end_date. If include_end is True, then end_date will be yielded. '''
209 current_date = start_date
210 while current_date < end_date:
211 yield current_date
212 current_date += timedelta(1)
213 if include_end:
214 yield end_date
215
216 def add_day_to_service(service_name, day):
217 try:
218 service = services[service_name]
219 except KeyError:
220 return
282 else: 221 else:
283 raise ValueError('an empty cluster has no center point') 222 service.dates.add(day)
284 return self._center 223 if day not in services_for_day:
285 def merge(self, other): 224 services_for_day[day] = set()
286 for bus_stop in other.stops: 225 services_for_day[day].add(service)
287 bus_stop.cluster = self 226 global viimeinen_käyttöpäivä
288 self.stops |= other.stops 227 viimeinen_käyttöpäivä = max(day, viimeinen_käyttöpäivä)
289 other.stops = set() 228
290 other._center = None 229 def filter_day(row, day):
291 def schedule(self, max_amount = 50): 230 day_names = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
292 result = [] 231 return int(row[day_names[day.isoweekday() - 1]])
293 for stop in self.stops: 232
294 result += stop.schedule(max_amount) 233 with gtfs_zip.open('calendar.txt') as file:
295 result.sort(key = lambda schedule_entry: schedule_entry['time']) 234 for row in read_csv(map(bytes.decode, file)):
296 return result[:max_amount] 235 for day in date_range(read_date(row['start_date']), read_date(row['end_date']), include_end = True):
297 236 if filter_day(row, day):
298 from collections import defaultdict 237 add_day_to_service(service_name = row['service_id'], day = day)
299 bus_stops_by_name = defaultdict(set) 238
300 for bus_stop in bus_stops.values(): 239 with gtfs_zip.open('calendar_dates.txt') as file:
301 bus_stops_by_name[bus_stop.name].add(bus_stop) 240 for row in read_csv(map(bytes.decode, file)):
302 bus_stops_by_name = dict(bus_stops_by_name) 241 add_day_to_service(service_name = row['service_id'], day = read_date(row['date']))
303 242
304 # ryhmittele bus_stops nimen mukaan 243 def services_available_at(day):
305 all_clusters = [] 244 for service in services.values():
306 def cluster_bus_stops(): 245 if day in service.dates:
307 sorted_bus_stops = sorted(bus_stops.values(), key = lambda bus_stop: bus_stop.name) 246 yield service
308 for bus_stop in sorted_bus_stops: 247
309 if not bus_stop.cluster: 248 print('Ladataan pysäkit... ', file = stderr, end = '', flush = True)
310 stops_to_cluster = {bus_stop} 249 with gtfs_zip.open('stops.txt') as file:
311 # etsi pysäkin samannimiset vastaparit 250 for row in read_csv(map(bytes.decode, file)):
312 for pair_candidate in bus_stops_by_name[bus_stop.name]: 251 location = Sijainti(float(row['stop_lat']), float(row['stop_lon']))
313 distance = pair_candidate.location.etäisyys(bus_stop.location) 252 stop = BusStop(
314 if pair_candidate is not bus_stop and distance <= 0.4: 253 reference = row['stop_id'],
315 stops_to_cluster.add(pair_candidate) 254 name = row['stop_name'],
316 for stop_to_cluster in stops_to_cluster: 255 location = location,
317 if stop_to_cluster.cluster: 256 code = row['stop_code'],
318 cluster = stop_to_cluster.cluster 257 )
319 break 258 bus_stops[stop.reference] = stop
259 with open('regions-per-stop.json') as file:
260 for stop_reference, region in json.load(file).items():
261 bus_stops[stop_reference].region = region
262 print('%d pysäkkiä' % len(bus_stops), file = stderr)
263
264
265 class BusStopCluster:
266 def __init__(self):
267 self.stops = set()
268 self._center = None
269 self.name = None
270 @property
271 def url_name(self):
272 return self.name.lower().replace('(', '').replace(')', '').replace(' ', '-')
273 def add_stop(self, stop):
274 assert not stop.cluster
275 stop.cluster = self
276 self.stops.add(stop)
277 self._center = None
278 @property
279 def center(self):
280 if not self._center:
281 if self.stops:
282 from statistics import median
283 pointtype = type(next(iter(self.stops)).location)
284 self._center = pointtype(
285 median(stop.location.x for stop in self.stops),
286 median(stop.location.y for stop in self.stops),
287 )
288 else:
289 raise ValueError('an empty cluster has no center point')
290 return self._center
291 def merge(self, other):
292 for bus_stop in other.stops:
293 bus_stop.cluster = self
294 self.stops |= other.stops
295 other.stops = set()
296 other._center = None
297 def schedule(self, max_amount = 50):
298 result = []
299 for stop in self.stops:
300 result += stop.schedule(max_amount)
301 result.sort(key = lambda schedule_entry: schedule_entry['time'])
302 return result[:max_amount]
303
304 from collections import defaultdict
305 bus_stops_by_name = defaultdict(set)
306 for bus_stop in bus_stops.values():
307 bus_stops_by_name[bus_stop.name].add(bus_stop)
308 bus_stops_by_name = dict(bus_stops_by_name)
309
310 # ryhmittele bus_stops nimen mukaan
311 all_clusters = []
312 def cluster_bus_stops():
313 sorted_bus_stops = sorted(bus_stops.values(), key = lambda bus_stop: bus_stop.name)
314 for bus_stop in sorted_bus_stops:
315 if not bus_stop.cluster:
316 stops_to_cluster = {bus_stop}
317 # etsi pysäkin samannimiset vastaparit
318 for pair_candidate in bus_stops_by_name[bus_stop.name]:
319 distance = pair_candidate.location.etäisyys(bus_stop.location)
320 if pair_candidate is not bus_stop and distance <= 0.4:
321 stops_to_cluster.add(pair_candidate)
322 for stop_to_cluster in stops_to_cluster:
323 if stop_to_cluster.cluster:
324 cluster = stop_to_cluster.cluster
325 break
326 else:
327 cluster = BusStopCluster()
328 all_clusters.append(cluster)
329 for stop_to_cluster in stops_to_cluster:
330 if not stop_to_cluster.cluster:
331 cluster.add_stop(stop_to_cluster)
332 # Merkitse muistiin pysäkkien vastaparit käyttäen hyväksi tämänhetkistä ryhmittelytietoa
333 for bus_stop in bus_stops.values():
334 if bus_stop.cluster:
335 bus_stop.pairs = bus_stop.cluster.stops - {bus_stop}
336 # Ryhmitä ne bus_stops, joilla ei ollut omaa vastaparia, muiden pysäkkien kanssa
337 for bus_stop in sorted_bus_stops:
338 if len(bus_stop.cluster.stops) == 1:
339 possibilities = set()
340 for cluster in all_clusters:
341 if cluster is not bus_stop.cluster:
342 distance = cluster.center.etäisyys(bus_stop.location)
343 if distance <= 0.4:
344 possibilities.add((distance, cluster))
345 if possibilities:
346 best = min(possibilities)[1]
347 all_clusters.remove(bus_stop.cluster)
348 best.merge(bus_stop.cluster)
349
350 def shared_elements_in_n_sets(sets):
351 from itertools import combinations
352 result = set()
353 for pair in combinations(sets, 2):
354 result |= pair[0] & pair[1]
355 return result
356
357 def name_clusters():
358 from collections import defaultdict
359 from pprint import pprint
360 clusters_per_name = defaultdict(set)
361 for cluster in all_clusters:
362 name_representing_stop = min((len(stop.reference), stop.reference, stop) for stop in cluster.stops)[2]
363 clusters_per_name[name_representing_stop.name].add(cluster)
364 for name, clusters in clusters_per_name.items():
365 if len(clusters) == 1:
366 # Ryhmä on ainoa jolla on varaus tälle nimelle. Sen kuin vaan.
367 next(iter(clusters)).name = name
368 else:
369 # Olisiko kaikki klusterit eri alueilla?
370 common_regions = shared_elements_in_n_sets({stop.region for stop in cluster.stops} for cluster in clusters)
371 # Esitys: ryhmä -> ne alueet jotka ovat tälle ryhmälle ainutlaatuisia
372 proposal = {
373 cluster: {stop.region for stop in cluster.stops} - common_regions - {None}
374 for cluster in clusters
375 }
376 # Jos enintään yksi klusteri tässä esityksessä on kokonaan ilman omaa aluetta, jolla se voisi eritellä,
377 # niin nimetään klusterit näiden alueiden mukaan.
378 # Se klusteri jolla ei ole omaa aluetta (jos on) jätetään ilman aluepäätettä.
379 if sum([1 for unique_areas in proposal.values() if not unique_areas]) <= 1:
380 for cluster, unique_areas in proposal.items():
381 individual_cluster_name = name
382 if unique_areas:
383 individual_cluster_name += ' (' + min(unique_areas) + ')'
384 cluster.name = individual_cluster_name
385 else:
386 # Typerä reunatapaus. Indeksoidaan numeroin...
387 for n, (_, cluster) in enumerate(sorted(
388 min((stop.reference.lower(), cluster) for stop in cluster.stops)
389 for cluster in clusters
390 ), 1):
391 individual_cluster_name = name + '-' + str(n)
392 cluster.name = individual_cluster_name
393
394 print('Ryhmitellään pysäkit...')
395 cluster_bus_stops()
396 name_clusters()
397
398 for cluster in all_clusters:
399 if cluster.url_name in clusters_by_name:
400 print('Warning: Clusters %r and %r share the same URL name: %r' % (cluster.name, clusters_by_name[cluster.url_name].name, cluster.url_name))
320 else: 401 else:
321 cluster = BusStopCluster() 402 clusters_by_name[cluster.url_name] = cluster
322 all_clusters.append(cluster) 403
323 for stop_to_cluster in stops_to_cluster: 404 print('Ladataan aikataulut... ', end = '', flush = True, file = stderr)
324 if not stop_to_cluster.cluster: 405 with gtfs_zip.open('stop_times.txt') as file:
325 cluster.add_stop(stop_to_cluster) 406 row_count = sum(line.count(b'\n') for line in file)
326 # Merkitse muistiin pysäkkien vastaparit käyttäen hyväksi tämänhetkistä ryhmittelytietoa 407 with gtfs_zip.open('stop_times.txt') as file:
327 for bus_stop in bus_stops.values(): 408 progress = 0
328 if bus_stop.cluster: 409 for row in read_csv(map(bytes.decode, file)):
329 bus_stop.pairs = bus_stop.cluster.stops - {bus_stop} 410 trip = all_trips[transform_trip_reference(row['trip_id'])]
330 # Ryhmitä ne bus_stops, joilla ei ollut omaa vastaparia, muiden pysäkkien kanssa 411 arrival_time = read_time(row['arrival_time'])
331 for bus_stop in sorted_bus_stops: 412 departure_time = read_time(row['departure_time'])
332 if len(bus_stop.cluster.stops) == 1: 413 stop = bus_stops[row['stop_id']]
333 possibilities = set() 414 traveled_distance = float(row['shape_dist_traveled']) * float(profile['metrics']['shape-modifier'])
334 for cluster in all_clusters: 415 trip.schedule.append(BusHalt(arrival_time, departure_time, stop, trip, traveled_distance))
335 if cluster is not bus_stop.cluster: 416 stop.involved_trips.add(trip)
336 distance = cluster.center.etäisyys(bus_stop.location) 417 progress += 1
337 if distance <= 0.4: 418 if progress % 1000 == 0:
338 possibilities.add((distance, cluster)) 419 print('\rLadataan aikataulut... %.1f%%' % (progress * 100 / row_count), end = ' ', file = stderr)
339 if possibilities: 420 print('\rLadataan aikataulut... ladattu', file = stderr)
340 best = min(possibilities)[1] 421
341 all_clusters.remove(bus_stop.cluster) 422 for trip in all_trips.values():
342 best.merge(bus_stop.cluster) 423 from busroute import simplify_name
343 424 schedule = trip.concise_schedule()
344 def shared_elements_in_n_sets(sets): 425 try:
345 from itertools import combinations 426 trip.from_place = simplify_name(schedule[0])
346 result = set() 427 trip.to_place = simplify_name(schedule[-1])
347 for pair in combinations(sets, 2): 428 except IndexError:
348 result |= pair[0] & pair[1] 429 trip.from_place = ''
349 return result 430 trip.to_place = ''
350 431
351 def name_clusters(): 432 for route in routes.values():
352 from collections import defaultdict 433 from collections import Counter
353 from pprint import pprint 434 from busroute import simplify_name
354 clusters_per_name = defaultdict(set) 435 tally = Counter()
355 for cluster in all_clusters: 436 for trip in route.trips:
356 name_representing_stop = min((len(stop.reference), stop.reference, stop) for stop in cluster.stops)[2] 437 schedule = trip.concise_schedule()
357 clusters_per_name[name_representing_stop.name].add(cluster) 438 places = set(schedule)
358 for name, clusters in clusters_per_name.items(): 439 do_add = True
359 if len(clusters) == 1: 440 assert type(schedule) is list
360 # Ryhmä on ainoa jolla on varaus tälle nimelle. Sen kuin vaan. 441 for candidate in tally:
361 next(iter(clusters)).name = name 442 if places.issubset(set(candidate)):
362 else: 443 do_add = False
363 # Olisiko kaikki klusterit eri alueilla? 444 tally.update({tuple(candidate)})
364 common_regions = shared_elements_in_n_sets({stop.region for stop in cluster.stops} for cluster in clusters) 445 if do_add:
365 # Esitys: ryhmä -> ne alueet jotka ovat tälle ryhmälle ainutlaatuisia 446 tally.update({tuple(schedule)})
366 proposal = { 447 try:
367 cluster: {stop.region for stop in cluster.stops} - common_regions - {None} 448 most_common_route = tally.most_common(1)[0][0]
368 for cluster in clusters 449 route.description = simplify_name(most_common_route[0]) + ' - ' + simplify_name(most_common_route[-1])
369 } 450 except:
370 # Jos enintään yksi klusteri tässä esityksessä on kokonaan ilman omaa aluetta, jolla se voisi eritellä, 451 route.description = ''
371 # niin nimetään klusterit näiden alueiden mukaan. 452 route.trips = sorted(route.trips, key = lambda trip: trip.schedule[0].departure_time)
372 # Se klusteri jolla ei ole omaa aluetta (jos on) jätetään ilman aluepäätettä. 453
373 if sum([1 for unique_areas in proposal.values() if not unique_areas]) <= 1: 454 # Fölin datassa on jotain tosi kummaa. Ilmeisesti ajovuoron viimeisen pysähdyksen saapumisaika on ihan täysin
374 for cluster, unique_areas in proposal.items(): 455 # väärin. Arvaan että se on seuraavan lähdön aika, mutta joka tapauksessa se on väärin.
375 individual_cluster_name = name 456 # Arvataan mikä se todellinen saapumisaika on. Se ei voi mennä kauhean paljon pahemmin vikaan kuin alkuperäinen
376 if unique_areas: 457 # väärin oleva data.
377 individual_cluster_name += ' (' + min(unique_areas) + ')' 458 for trip in all_trips.values():
378 cluster.name = individual_cluster_name 459 bus_speed_coefficient = 750 # metriä minuutissa
379 else: 460 last_leg_distance = trip.schedule[-1].traveled_distance - trip.schedule[-2].traveled_distance
380 # Typerä reunatapaus. Indeksoidaan numeroin... 461 trip.schedule[-1].arrival_time = trip.schedule[-2].departure_time + timedelta(minutes = last_leg_distance / bus_speed_coefficient)
381 for n, (_, cluster) in enumerate(sorted( 462
382 min((stop.reference.lower(), cluster) for stop in cluster.stops) 463 if __name__ == '__main__':
383 for cluster in clusters 464 from configparser import ConfigParser
384 ), 1): 465 profile = ConfigParser()
385 individual_cluster_name = name + '-' + str(n) 466 profile.read('profiles/föli.ini')
386 cluster.name = individual_cluster_name 467 load_buses('gtfs.zip', profile)
387
388 print('Ryhmitellään pysäkit...')
389 cluster_bus_stops()
390 name_clusters()
391
392 clusters_by_name = {}
393 for cluster in all_clusters:
394 if cluster.url_name in clusters_by_name:
395 print('Warning: Clusters %r and %r share the same URL name: %r' % (cluster.name, clusters_by_name[cluster.url_name].name, cluster.url_name))
396 else:
397 clusters_by_name[cluster.url_name] = cluster
398
399 print('Ladataan aikataulut... ', end = '', flush = True, file = stderr)
400 with open('gtfs/stop_times.txt') as file:
401 row_count = sum(line.count('\n') for line in file)
402 progress = 0
403 file.seek(0)
404 for row in read_csv(file):
405 trip = all_trips[transform_trip_reference(row['trip_id'])]
406 arrival_time = read_time(row['arrival_time'])
407 departure_time = read_time(row['departure_time'])
408 stop = bus_stops[row['stop_id']]
409 traveled_distance = float(row['shape_dist_traveled'])
410 trip.schedule.append(BusHalt(arrival_time, departure_time, stop, trip, traveled_distance))
411 stop.involved_trips.add(trip)
412 progress += 1
413 if progress % 1000 == 0:
414 print('\rLadataan aikataulut... %.1f%%' % (progress * 100 / row_count), end = ' ', file = stderr)
415 print('\rLadataan aikataulut... ladattu', file = stderr)
416
417 for trip in all_trips.values():
418 from busroute import simplify_name
419 schedule = trip.concise_schedule()
420 try:
421 trip.from_place = simplify_name(schedule[0])
422 trip.to_place = simplify_name(schedule[-1])
423 except IndexError:
424 trip.from_place = ''
425 trip.to_place = ''
426
427 for route in routes.values():
428 from collections import Counter
429 from busroute import simplify_name
430 tally = Counter()
431 for trip in route.trips:
432 schedule = trip.concise_schedule()
433 places = set(schedule)
434 do_add = True
435 assert type(schedule) is list
436 for candidate in tally:
437 if places.issubset(set(candidate)):
438 do_add = False
439 tally.update({tuple(candidate)})
440 if do_add:
441 tally.update({tuple(schedule)})
442 try:
443 most_common_route = tally.most_common(1)[0][0]
444 route.description = simplify_name(most_common_route[0]) + ' - ' + simplify_name(most_common_route[-1])
445 except:
446 route.description = ''
447 route.trips = sorted(route.trips, key = lambda trip: trip.schedule[0].departure_time)

mercurial