Effect method
A measured effect compares a port's or chokepoint's daily series during a disruption with the same weekdays in a 56 day baseline that ends 8 days before the event. It reports the expected and observed daily averages, the absolute and percent change, and a standardised score, always together. This is method wfm-effect-1.0, written 2026-09-14 (14 September 2026). This is a measured change in public data during the event window, compared with a baseline before it. It shows timing, not cause. We describe an event as causing a change only when an official source says so.
Status
As of 2026-09-16 (16 September 2026) the engine that applies this method is built, running weekly, and computing real figures, with the unit tests and the reproduction script from sections 6 and 9 in place. Measured effects now appear on event pages for every approved event whose named series and entity have public data behind them; the rest of the drafted backfill archive is still at the publication gate described in the methodology, awaiting review. CSV files of IMF PortWatch values stay unpublished until the IMF confirms that redistribution is permitted; see sources and licences.
Contents
The method
This is how World Freight Monitor puts a number on what a disruption did to a public data series. It is written so that anyone with the published CSV for a series and the event’s published windows can reproduce every figure by hand or in a spreadsheet. If you cannot, that is a bug and we want to know.
1. What an effect is, and is not
An effect compares what a series did during an event window with what the same series would ordinarily have done on the same weekdays, judged from a recent baseline period before the event.
An effect is a measured coincidence in time. It is not proof that the event caused the change. Other things move port calls and canal transits: carrier schedules, holidays, demand swings, other disruptions elsewhere, AIS coverage gaps. Every effect block on the site carries this sentence, verbatim:
This is a measured change in public data during the event window, compared with a baseline before it. It shows timing, not cause. We describe an event as causing a change only when an official source says so.
We never write “caused by” about an effect unless the event’s cause field cites an official source making that attribution, and then we quote whom.
2. Inputs
- A daily series
x(t)for one entity (one port or one chokepoint), fromdata/registry/series.yaml. In v1 these are the IMF PortWatch daily series: port calls, import and export volume estimates, and chokepoint transit counts and capacity. - The observations at a fixed
data_revision. The CSV at/data/<series>.csvpublishesdate, entity_id, value, revisionso the inputs are recoverable. - The event’s start and end dates, and the windows of every other published event touching the same entity.
- The holiday calendar
data/registry/calendar.yaml(section 4).
Monthly series (BLS, NY Fed, port authority throughput) are not used for event effects in v1. They are displayed as context only. A month is too coarse to isolate most disruptions, and pretending otherwise would overstate precision.
3. Windows
All windows are inclusive and in calendar days.
Event window W.
window_start=start_date. Whenstart_precisionisweekormonth, effects are not computed andno_effect_reasonisbelow_resolution, unless the editor supplies explicitwindow_startandwindow_endwith a written reason.window_end=end_date, or the latest date with data if the event is ongoing, or ifend_dateis after the latest data. In those cases the effect is markedprovisional.- Maximum length 120 days. Longer events are measured in consecutive 28 day segments plus the full window, so a slow build or slow recovery stays visible.
- An editor may override the window in
effect_targetswith a stated reason, for example when a port reopens but a queue takes two weeks to clear. Overrides are shown on the page.
Guard gap G. The 7 days immediately before window_start are excluded from the baseline. Cargo is often rushed ahead of an announced strike or storm, and including that surge would inflate the baseline.
Baseline window B. The 56 days ending at window_start - 8. So for an event starting on day 0, the baseline is days -63 through -8.
Exclusions inside B. A day is removed from the baseline if:
- it falls inside the window of another published event that lists the same entity (
reason: other_event:<id>), unless that event is ambient (below); - it falls inside a calendar exclusion for the entity’s country (
reason: holiday:<name>); - the observation is missing (
reason: missing).
Ambient events (from wfm-effect-1.1). Another published event on the same entity is ambient when it was already in force for the whole of the longest possible baseline and is still in force when the baseline ends: it started at least 112 days before the baseline’s last day and its window (or, if it is ongoing, the latest data) reaches that day. An ambient event is the state the new event is measured against, so it does not exclude baseline days. If it did, a new event on an entity with a long-running disruption, for example a Houthi embargo at Bab el-Mandeb during the Red Sea diversion that has run since 2023-11-19, could never have a baseline. The effect is then the change on top of the earlier disruption, not the change from normal, and the page says so and names the ambient events. Any event that started later, or ended before the baseline ends, is not ambient and excludes its days exactly as before. Rows that use an ambient event carry method_id wfm-effect-1.1; every other row is computed exactly as in 1.0 and keeps wfm-effect-1.0.
Minimum baseline. After exclusions B needs at least 28 valid days and at least 3 valid days for every weekday. If not, B extends backwards 7 days at a time up to 112 days total. If it still falls short, no effect is computed and the reason insufficient_baseline is shown.
Why 56 days and not a year-ago comparison: a baseline just before the event reflects the current service network, fleet deployment and demand, which change a lot year to year. Eight weeks is long enough for a stable median and short enough to be current. Its weakness is annual seasonality, dealt with in section 4.
4. Seasonality, holidays and Lunar New Year
Weekly seasonality is handled directly by comparing like weekday with like weekday (section 5).
Holidays are handled by exclusion, not adjustment. calendar.yaml lists, per country, dated exclusion ranges. v1 contents:
- Lunar New Year, for CN, HK, TW, KR, SG, VN, MY: from 7 days before to 21 days after the first day of the lunar new year. Factory closures cut export volumes for weeks, then volumes rebound.
- China National Day Golden Week, CN and HK: 1 to 7 October, plus 3 days after.
- Christmas and New Year, all countries: 24 December to 2 January.
- Eid al-Fitr and Eid al-Adha, for EG, SA, AE, OM, YE, DJ, PK, MY, ID, TR: the official public holiday dates plus 1 day either side.
- National public holidays for the country of each covered port, from official government calendars, where a port closes or runs reduced shifts.
If an event window itself overlaps a calendar exclusion, the effect is still computed, since the event days are what we are measuring, but the page states the overlap plainly and confidence notes it. Measuring a Shanghai closure that runs through Lunar New Year against a pre-holiday baseline would overstate the fall, so the review checklist requires the editor to either shorten the window, add the year-ago comparison below, or set no_effect_reason.
Year-ago comparison (secondary). For windows of 28 days or more, or any window that overlaps a calendar exclusion, we also report the same statistic computed against a baseline made of the same calendar dates 364 days earlier (364 keeps weekdays aligned), with the same exclusion rules applied to that year. It is shown beneath the headline figure, labelled as a secondary check. If the two disagree in sign, the page says so.
5. Statistics
For each date t in B (after exclusions), let wd(t) be its weekday.
Weekday medians. For each weekday k, m_k = median of x(t) over valid baseline days with wd(t) = k.
Expected value. For each date t in W, e(t) = m_{wd(t)}.
Residuals and spread. For each valid baseline day, r(t) = x(t) - m_{wd(t)}. MAD = median of |r(t) - median(r)|. The robust spread is s = 1.4826 * MAD. A floor stops tiny spreads producing huge scores:
- for count series (
kind: countin series.yaml, for example port calls and transits):s = max(s, sqrt(max(median of all valid baseline x, 1))); - for continuous series (
kind: continuous, for example tonnage estimates):s = max(s, 0.05 * median of all valid baseline x).
Missing days inside W are skipped, and window_days counts only days with data.
Reported figures.
| Name | Formula | Shown as |
|---|---|---|
baseline_value |
mean of e(t) over valid t in W |
Expected daily average, with unit |
observed_value |
mean of x(t) over valid t in W |
Observed daily average, with unit |
abs_change |
observed_value - baseline_value |
Signed, with unit |
delta_pct |
100 * abs_change / baseline_value |
Signed percent, one decimal. Null if baseline_value is below the series pct_floor (default 3 for counts), because percentages of tiny numbers mislead. |
z_score |
mean over valid t in W of (x(t) - e(t)) / s |
Signed, one decimal, labelled “standardised score” |
The standardised score is the average daily deviation, measured in baseline spreads. We deliberately do not multiply by the square root of the window length. Daily port data is strongly autocorrelated, so a textbook standard error would make every long event look astronomically significant. Our score answers a plainer question: on a typical day of the event, how unusual was the day?
Rounding happens only at display. Stored values keep full precision.
Never a single number. Every rendered effect shows, together: baseline value, observed value, absolute change, percent change (or why it is omitted), the standardised score, both windows with dates, the number of valid days in each, the method id, the computation date, and the caveat sentence in section 1.
6. Worked example (synthetic)
A port averages 20 calls a day on weekdays and 10 at weekends in its 56 day baseline, with a median absolute deviation of residuals of 2. A strike runs for 4 days, Monday to Thursday, with 6, 5, 7, 6 calls.
e(t)= 20 each day, sobaseline_value= 20.0 calls per day.observed_value= (6 + 5 + 7 + 6) / 4 = 6.0.abs_change= -14.0;delta_pct= -70.0%.s= 1.4826 * 2 = 2.965; floor is sqrt(median x). With 40 weekday values at 20 and 16 weekend values at 10 the median is 20, so the floor is 4.472, ands= 4.472.- Daily scores: (6-20)/4.472 = -3.13, -3.35, -2.91, -3.13;
z_score= -3.13.
This exact case is a unit test (pipeline/effect/tests/test_worked_example.py), along with synthetic series for a step drop, a slow recovery, a holiday overlap, a missing-data gap, an overlapping second event, and a zero-spread series.
7. Detection is not publication
The same machinery runs nightly as a detector across every covered entity and series, using a rolling window:
W= the latest 7 days with data;Band exclusions as above.- A candidate is raised when
z_scoreis at or below -3.0, or at or above +3.0 for chokepoint transits (diversion onto an alternative route shows up as a rise), on 3 consecutive nightly runs, and|delta_pct|is 25 or more, and the baseline median is at least 3. - Candidates go to the
candidatestable with the entity, dates, statistics and a chart link. They are never written toeventsand never shown on the site.
A threshold crossing means “a human should look”. It does not mean anything happened. AIS gaps, a PortWatch methodology change, or an ordinary schedule change can all trip it. Publication happens only when an editor finds sources, writes the record and approves it.
Alert sensitivity for watchlists maps onto the same score: low sends when |z_score| is 4.0 or more, medium at 3.0, high at 2.0, always on the 7 day rolling window, and never more than one email per user per day.
8. Versioning, revisions and recomputation
- Every stored effect row has
method_id,computed_atanddata_revision.data_revisionis<highest observation revision number used>:<first 12 hex characters of the SHA-256 of the canonical input>, where the canonical input is the sorteddate,valuelines forWandBjoined by newlines. - Effects recompute nightly for ongoing events, weekly for all events, and immediately when any observation inside
WorBgets a new revision. - A recomputation that produces a different
data_revisionor a differentmethod_idinserts a new row and setssuperseded_byon the old one. Rows are never updated in place and never deleted. The page shows the current value, the date it was computed, and a “previous computations” disclosure listing every earlier value with its date and revision. - A change to anything in this document that could change a number increments the method id: patch (
1.0.1) for bug fixes, minor (1.1) for new exclusions or floors, major (2.0) for a different statistic. The changelog at the end of this file records each version. Old method ids remain reproducible from the tagged commit.
9. Reproducing a number
- Download
/data/<series_id>.csvand filter to theentity_id. - Open
/data/events.json, find the event, and readmeasured_effect[]forwindow_start,window_end,baseline_start,baseline_endandexcluded_dates. - Keep baseline rows between
baseline_startandbaseline_end, dropexcluded_dates, and use the revision matching thedata_revisionnumber (the CSV keeps superseded revisions in/data/<series_id>-revisions.csv). - Apply section 5.
pipeline/effect/reproduce.py does exactly these four steps from the public files only, with no database, and is part of the test suite.
10. Known limits
- AIS-derived counts miss vessels with transponders off, which matters most in security incidents and sanctions contexts. PortWatch documents its own coverage limits; our method inherits them.
- PortWatch volume estimates are modelled from vessel draft and capacity, not customs records.
- A port with only a few calls a day cannot show a subtle effect; the floors in section 5 make its scores conservative by design.
- A row measured against an ambient event (
wfm-effect-1.1) says how much the entity moved on top of the earlier disruption. It cannot say what the level would have been without both, and two overlapping disruptions cannot be separated by this method. - Chokepoint transits fall both when a route is blocked and when demand drops. The number cannot tell those apart. The narrative and sources must.
Changelog
wfm-effect-1.0, 2026-09-14: first version.wfm-effect-1.1, 2026-09-20: ambient events (section 3). An earlier event in force for the whole baseline no longer excludes its days. Only rows that use this rule are re-identified as 1.1; all others are numerically identical to 1.0 and were not recomputed.