191 pysäkit[pysäkki.tunniste] = pysäkki |
194 pysäkit[pysäkki.tunniste] = pysäkki |
192 with open('regions-per-stop.json') as file: |
195 with open('regions-per-stop.json') as file: |
193 for pysäkkitunniste, alue in json.load(file).items(): |
196 for pysäkkitunniste, alue in json.load(file).items(): |
194 pysäkit[pysäkkitunniste].alue = alue |
197 pysäkit[pysäkkitunniste].alue = alue |
195 print('%d pysäkkiä' % len(pysäkit), file = stderr) |
198 print('%d pysäkkiä' % len(pysäkit), file = stderr) |
|
199 |
|
200 class BusStopCluster: |
|
201 def __init__(self): |
|
202 self.stops = set() |
|
203 self._center = None |
|
204 self.name = None |
|
205 def add_stop(self, stop): |
|
206 assert not stop.cluster |
|
207 stop.cluster = self |
|
208 self.stops.add(stop) |
|
209 self._center = None |
|
210 @property |
|
211 def center(self): |
|
212 if not self._center: |
|
213 if self.stops: |
|
214 from statistics import median |
|
215 pointtype = type(next(iter(self.stops)).sijainti) |
|
216 self._center = pointtype( |
|
217 median(stop.sijainti.x for stop in self.stops), |
|
218 median(stop.sijainti.y for stop in self.stops), |
|
219 ) |
|
220 else: |
|
221 raise ValueError('an empty cluster has no center point') |
|
222 return self._center |
|
223 def merge(self, other): |
|
224 for bus_stop in other.stops: |
|
225 bus_stop.cluster = self |
|
226 self.stops |= other.stops |
|
227 other.stops = set() |
|
228 other._center = None |
|
229 |
|
230 from collections import defaultdict |
|
231 bus_stops_by_name = defaultdict(set) |
|
232 for bus_stop in pysäkit.values(): |
|
233 bus_stops_by_name[bus_stop.nimi].add(bus_stop) |
|
234 bus_stops_by_name = dict(bus_stops_by_name) |
|
235 |
|
236 # ryhmittele pysäkit nimen mukaan |
|
237 all_clusters = [] |
|
238 def cluster_bus_stops(): |
|
239 sorted_bus_stops = sorted(pysäkit.values(), key = lambda bus_stop: bus_stop.nimi) |
|
240 for bus_stop in sorted_bus_stops: |
|
241 if not bus_stop.cluster: |
|
242 stops_to_cluster = {bus_stop} |
|
243 # etsi pysäkin samannimiset vastaparit |
|
244 for pair_candidate in bus_stops_by_name[bus_stop.nimi]: |
|
245 distance = pair_candidate.sijainti.etäisyys(bus_stop.sijainti) |
|
246 if pair_candidate is not bus_stop and distance <= 0.3: |
|
247 stops_to_cluster.add(pair_candidate) |
|
248 for stop_to_cluster in stops_to_cluster: |
|
249 if stop_to_cluster.cluster: |
|
250 cluster = stop_to_cluster.cluster |
|
251 break |
|
252 else: |
|
253 cluster = BusStopCluster() |
|
254 all_clusters.append(cluster) |
|
255 for stop_to_cluster in stops_to_cluster: |
|
256 if not stop_to_cluster.cluster: |
|
257 cluster.add_stop(stop_to_cluster) |
|
258 # Merkitse muistiin pysäkkien vastaparit käyttäen hyväksi tämänhetkistä ryhmittelytietoa |
|
259 for bus_stop in pysäkit.values(): |
|
260 if bus_stop.cluster: |
|
261 bus_stop.pairs = bus_stop.cluster.stops - {bus_stop} |
|
262 # Ryhmitä ne pysäkit, joilla ei ollut omaa vastaparia, muiden pysäkkien kanssa |
|
263 for bus_stop in sorted_bus_stops: |
|
264 if len(bus_stop.cluster.stops) == 1: |
|
265 possibilities = set() |
|
266 for cluster in all_clusters: |
|
267 if cluster is not bus_stop.cluster: |
|
268 distance = cluster.center.etäisyys(bus_stop.sijainti) |
|
269 if distance <= 0.3: |
|
270 possibilities.add((distance, cluster)) |
|
271 if possibilities: |
|
272 best = min(possibilities)[1] |
|
273 all_clusters.remove(bus_stop.cluster) |
|
274 best.merge(bus_stop.cluster) |
|
275 |
|
276 def shared_elements_in_n_sets(sets): |
|
277 from itertools import combinations |
|
278 result = set() |
|
279 for pair in combinations(sets, 2): |
|
280 result |= pair[0] & pair[1] |
|
281 return result |
|
282 |
|
283 def name_clusters(): |
|
284 from collections import defaultdict |
|
285 from pprint import pprint |
|
286 clusters_per_name = defaultdict(set) |
|
287 for cluster in all_clusters: |
|
288 name_representing_stop = min((len(pysäkki.tunniste), pysäkki.tunniste, pysäkki) for pysäkki in cluster.stops)[2] |
|
289 name = name_representing_stop.nimi.lower().replace(' ', '-') |
|
290 clusters_per_name[name].add(cluster) |
|
291 for name, clusters in clusters_per_name.items(): |
|
292 if len(clusters) == 1: |
|
293 # Ryhmä on ainoa jolla on varaus tälle nimelle. Sen kuin vaan. |
|
294 next(iter(clusters)).name = name |
|
295 else: |
|
296 # Olisiko kaikki klusterit eri alueilla? |
|
297 common_regions = shared_elements_in_n_sets({stop.alue for stop in cluster.stops} for cluster in clusters) |
|
298 # Esitys: ryhmä -> ne alueet jotka ovat tälle ryhmälle ainutlaatuisia |
|
299 proposal = { |
|
300 cluster: {stop.alue for stop in cluster.stops} - common_regions - {None} |
|
301 for cluster in clusters |
|
302 } |
|
303 # Jos enintään yksi klusteri tässä esityksessä on kokonaan ilman omaa aluetta, jolla se voisi eritellä, |
|
304 # niin nimetään klusterit näiden alueiden mukaan. |
|
305 # Se klusteri jolla ei ole omaa aluetta (jos on) jätetään ilman aluepäätettä. |
|
306 if sum([1 for unique_areas in proposal.values() if not unique_areas]) <= 1: |
|
307 for cluster, unique_areas in proposal.items(): |
|
308 individual_cluster_name = name |
|
309 if unique_areas: |
|
310 individual_cluster_name += '-' + min(unique_areas).lower().replace(' ', '-') |
|
311 cluster.name = individual_cluster_name |
|
312 else: |
|
313 # Typerä reunatapaus. Indeksoidaan numeroin... |
|
314 for n, (_, cluster) in enumerate(sorted( |
|
315 min((stop.tunniste.lower(), cluster) for stop in cluster.stops) |
|
316 for cluster in clusters |
|
317 ), 1): |
|
318 individual_cluster_name = name + '-' + str(n) |
|
319 cluster.name = individual_cluster_name |
|
320 |
|
321 print('Ryhmitellään pysäkit...') |
|
322 cluster_bus_stops() |
|
323 name_clusters() |
196 |
324 |
197 print('Ladataan aikataulut... ', end = '', flush = True, file = stderr) |
325 print('Ladataan aikataulut... ', end = '', flush = True, file = stderr) |
198 with open('gtfs/stop_times.txt') as file: |
326 with open('gtfs/stop_times.txt') as file: |
199 rivimäärä = sum(line.count('\n') for line in file) |
327 rivimäärä = sum(line.count('\n') for line in file) |
200 laskettu = 0 |
328 laskettu = 0 |