microsegments 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. {microsegments-0.2.0 → microsegments-0.2.1}/PKG-INFO +1 -1
  2. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/_version.py +2 -2
  3. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/io/coverage.py +24 -1
  4. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_coverage.py +21 -0
  5. {microsegments-0.2.0 → microsegments-0.2.1}/.github/workflows/publish.yml +0 -0
  6. {microsegments-0.2.0 → microsegments-0.2.1}/.github/workflows/test.yml +0 -0
  7. {microsegments-0.2.0 → microsegments-0.2.1}/.gitignore +0 -0
  8. {microsegments-0.2.0 → microsegments-0.2.1}/LICENSE +0 -0
  9. {microsegments-0.2.0 → microsegments-0.2.1}/README.md +0 -0
  10. {microsegments-0.2.0 → microsegments-0.2.1}/docs/report.jpg +0 -0
  11. {microsegments-0.2.0 → microsegments-0.2.1}/examples/gtfsrt_csv.toml +0 -0
  12. {microsegments-0.2.0 → microsegments-0.2.1}/examples/make_synth.py +0 -0
  13. {microsegments-0.2.0 → microsegments-0.2.1}/examples/stib1_platform.toml +0 -0
  14. {microsegments-0.2.0 → microsegments-0.2.1}/examples/stib55.toml +0 -0
  15. {microsegments-0.2.0 → microsegments-0.2.1}/examples/stib55_full.toml +0 -0
  16. {microsegments-0.2.0 → microsegments-0.2.1}/examples/stib71_platform.toml +0 -0
  17. {microsegments-0.2.0 → microsegments-0.2.1}/examples/synth.toml +0 -0
  18. {microsegments-0.2.0 → microsegments-0.2.1}/pyproject.toml +0 -0
  19. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/__init__.py +0 -0
  20. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/aggregate.py +0 -0
  21. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/cli.py +0 -0
  22. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/compare.py +0 -0
  23. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/config.py +0 -0
  24. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/contract.py +0 -0
  25. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/hotspots.py +0 -0
  26. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/html/__init__.py +0 -0
  27. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/html/export.py +0 -0
  28. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/html/leaflet.min.css +0 -0
  29. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/html/template.html +0 -0
  30. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/io/__init__.py +0 -0
  31. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/io/events.py +0 -0
  32. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/io/gtfsrt.py +0 -0
  33. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/io/ids.py +0 -0
  34. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/io/stib.py +0 -0
  35. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/io/tabular.py +0 -0
  36. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/io/timeutil.py +0 -0
  37. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/locate/__init__.py +0 -0
  38. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/locate/common.py +0 -0
  39. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/locate/linear.py +0 -0
  40. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/locate/mapmatch.py +0 -0
  41. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/locate/resample.py +0 -0
  42. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/metrics.py +0 -0
  43. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/network/__init__.py +0 -0
  44. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/network/calendar.py +0 -0
  45. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/network/geometry.py +0 -0
  46. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/network/keys.py +0 -0
  47. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/network/patterns.py +0 -0
  48. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/pipeline.py +0 -0
  49. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/plot.py +0 -0
  50. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/py.typed +0 -0
  51. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/report.py +0 -0
  52. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/schema.py +0 -0
  53. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/segments.py +0 -0
  54. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/simulate.py +0 -0
  55. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/tracks.py +0 -0
  56. {microsegments-0.2.0 → microsegments-0.2.1}/src/microsegments/tune.py +0 -0
  57. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/golden/buckets.parquet +0 -0
  58. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/golden/drops.parquet +0 -0
  59. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/golden/flen.parquet +0 -0
  60. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/golden/obs_day.parquet +0 -0
  61. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/golden/obs_dow.parquet +0 -0
  62. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/golden/passages.parquet +0 -0
  63. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/golden/summary.json +0 -0
  64. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/golden/validation.parquet +0 -0
  65. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/gtfs/agency.parquet +0 -0
  66. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/gtfs/calendar.parquet +0 -0
  67. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/gtfs/calendar_dates.parquet +0 -0
  68. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/gtfs/routes.parquet +0 -0
  69. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/gtfs/shapes.parquet +0 -0
  70. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/gtfs/stop_times.parquet +0 -0
  71. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/gtfs/stops.parquet +0 -0
  72. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/gtfs/trips.parquet +0 -0
  73. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/punctuality/route55.parquet +0 -0
  74. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/speed55_ref.json +0 -0
  75. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/vd55/20250318.parquet +0 -0
  76. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/vd55/20250318.snaps.parquet +0 -0
  77. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/vd55/20250319.parquet +0 -0
  78. {microsegments-0.2.0 → microsegments-0.2.1}/tests/fixtures/stib55/vd55/20250319.snaps.parquet +0 -0
  79. {microsegments-0.2.0 → microsegments-0.2.1}/tests/gtfs_fixtures.py +0 -0
  80. {microsegments-0.2.0 → microsegments-0.2.1}/tests/regression/make_golden.py +0 -0
  81. {microsegments-0.2.0 → microsegments-0.2.1}/tests/t4_fixtures.py +0 -0
  82. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_aggregate.py +0 -0
  83. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_compare.py +0 -0
  84. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_hotspots.py +0 -0
  85. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_hotspots_criteria.py +0 -0
  86. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_io_events.py +0 -0
  87. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_io_gtfsrt.py +0 -0
  88. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_io_stib.py +0 -0
  89. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_io_tabular.py +0 -0
  90. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_locate_linear.py +0 -0
  91. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_locate_mapmatch.py +0 -0
  92. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_metrics.py +0 -0
  93. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_network_geometry.py +0 -0
  94. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_network_patterns.py +0 -0
  95. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_pipeline_cli.py +0 -0
  96. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_regression_stib55.py +0 -0
  97. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_report_html.py +0 -0
  98. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_scaffold.py +0 -0
  99. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_segments.py +0 -0
  100. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_simulate.py +0 -0
  101. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_tracks.py +0 -0
  102. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_tune.py +0 -0
  103. {microsegments-0.2.0 → microsegments-0.2.1}/tests/test_versions_merge.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: microsegments
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Where do transit vehicles linger? Count vehicle position observations per micro-segment of a line, from GTFS + AVL / GTFS-RT positions.
5
5
  Project-URL: Homepage, https://github.com/GaspardMerten/microsegments
6
6
  Project-URL: Repository, https://github.com/GaspardMerten/microsegments
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.0'
22
- __version_tuple__ = version_tuple = (0, 2, 0)
21
+ __version__ = version = '0.2.1'
22
+ __version_tuple__ = version_tuple = (0, 2, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -5,6 +5,12 @@ non-frozen poll (the last poll covers min(tick_s, gap_cap_s)). Each frozen snaps
5
5
  [t_i, t_i + min(t_next_any - t_i, gap_cap_s)) of ``frozen_s`` (t_next_any: next snapshot of any kind).
6
6
  Intervals are split exactly at local hour boundaries and credited to the hour they fall in.
7
7
  ``coverage = covered_s / 3600`` clipped to 1. Frozen snapshots never count as polls.
8
+
9
+ FROZEN flag: an hour is flagged when it overlaps a *stall*, a run of consecutive frozen snapshots
10
+ lasting more than ``max_frozen_s`` (each covering min(next snapshot - t, gap_cap_s)). Isolated
11
+ repeats are normal for feeds that refresh more slowly than they are polled (STIB since 2026: about
12
+ one poll in five repeats the previous one, 300-700 s per hour in total) and do not flag the hour;
13
+ ``frozen_s`` still reports their total.
8
14
  """
9
15
  from __future__ import annotations
10
16
 
@@ -79,9 +85,22 @@ def coverage(snapshots: pl.DataFrame | pl.LazyFrame, tz: str = "Europe/Brussels"
79
85
  tf, gf = t[fr], (nxt_any - t)[fr]
80
86
  fs, fl = _split_by_hour(tf, tf + np.minimum(gf, cap), tz)
81
87
 
88
+ # stalls: runs of consecutive frozen snapshots longer than max_frozen_s
89
+ ss, sl = np.zeros(0), np.zeros(0)
90
+ if fr.any():
91
+ span = np.minimum(nxt_any - t, cap)
92
+ edge = np.diff(np.concatenate([[0], fr.astype(np.int8), [0]]))
93
+ r0, r1 = np.flatnonzero(edge == 1), np.flatnonzero(edge == -1) # [r0, r1) frozen runs
94
+ csum = np.concatenate([[0.0], np.cumsum(np.where(fr, span, 0.0))])
95
+ dur = csum[r1] - csum[r0]
96
+ long = dur > max_frozen_s
97
+ if long.any():
98
+ ss, sl = _split_by_hour(t[r0[long]], t[r0[long]] + dur[long], tz)
99
+
82
100
  sds = service_day_start
83
101
  cov = _hour_table(ps, pl_, "covered_s", tz, sds)
84
102
  frz = _hour_table(fs, fl, "frozen_s", tz, sds)
103
+ stall = _hour_table(ss, sl, "stall_s", tz, sds) if ss.size else None
85
104
  nsn = _hour_table(tp, np.ones_like(tp), "n_snapshots", tz, sds)
86
105
 
87
106
  if dates is None:
@@ -96,9 +115,13 @@ def coverage(snapshots: pl.DataFrame | pl.LazyFrame, tz: str = "Europe/Brussels"
96
115
  .join(frz, on=["service_date", "hour"], how="left")
97
116
  .join(nsn, on=["service_date", "hour"], how="left")
98
117
  .with_columns(pl.col("covered_s", "frozen_s", "n_snapshots").fill_null(0.0)))
118
+ if stall is not None:
119
+ out = out.join(stall, on=["service_date", "hour"], how="left").with_columns(pl.col("stall_s").fill_null(0.0))
120
+ else:
121
+ out = out.with_columns(pl.lit(0.0).alias("stall_s"))
99
122
  c = (pl.col("covered_s") / 3600.0).clip(0.0, 1.0)
100
123
  flags = (pl.when(c < min_coverage).then(Flag.LOW_COVERAGE).otherwise(0)
101
- + pl.when(pl.col("frozen_s") > max_frozen_s).then(Flag.FROZEN).otherwise(0))
124
+ + pl.when(pl.col("stall_s") > 0).then(Flag.FROZEN).otherwise(0))
102
125
  out = out.with_columns(c.alias("coverage"), flags.alias("flags"))
103
126
  return out.select([pl.col(k).cast(v) for k, v in COVERAGE.items()]).sort("service_date", "hour")
104
127
 
@@ -101,6 +101,27 @@ def test_frozen_not_counted():
101
101
  assert c2.filter(pl.col("hour") == 10)["covered_s"][0] == row["covered_s"]
102
102
 
103
103
 
104
+ def test_scattered_repeats_do_not_flag_frozen():
105
+ """One poll in four repeating the previous one (a feed refreshed more slowly than polled) is not a
106
+ stall: ~900 s of frozen snapshots in the hour, no run longer than 20 s."""
107
+ t = _day_polls()
108
+ frozen = (np.arange(t.size) % 4) == 3
109
+ c = coverage(_snaps(t, frozen), TZ, gap_cap_s=40, max_frozen_s=300)
110
+ row = c.filter(pl.col("hour") == 10).row(0, named=True)
111
+ assert row["frozen_s"] > 300
112
+ assert not row["flags"] & Flag.FROZEN and not row["flags"] & Flag.LOW_COVERAGE
113
+
114
+
115
+ def test_stall_across_hours_flags_both():
116
+ t = _day_polls()
117
+ f1, f2 = local_epoch(DAY, 10 * 3600 + 3400, TZ), local_epoch(DAY, 11 * 3600 + 200, TZ) # 400 s stall
118
+ c = coverage(_snaps(t, (t >= f1) & (t < f2)), TZ, gap_cap_s=40, max_frozen_s=300)
119
+ fl = dict(zip(c["hour"].to_list(), c["flags"].to_list()))
120
+ assert fl[10] & Flag.FROZEN and fl[11] & Flag.FROZEN and not fl[12] & Flag.FROZEN
121
+ c = coverage(_snaps(t, (t >= f1) & (t < f1 + 200)), TZ, gap_cap_s=40, max_frozen_s=300) # 200 s: fine
122
+ assert not any(f & Flag.FROZEN for f in c["flags"].to_list())
123
+
124
+
104
125
  def test_silent_hours_are_zero_rows():
105
126
  t = _day_polls()
106
127
  t = t[(t < local_epoch(DAY, 12 * 3600, TZ)) | (t >= local_epoch(DAY, 14 * 3600, TZ))]
File without changes
File without changes
File without changes