tempolocus looks at time-series activity patterns to infer a location.
tempolocus accepts two JSON shapes plus timestamp-list imports:
- Weekly hourly buckets: a list of objects containing
day,hour, andcount. - Yearly daily buckets: an object containing
year,max, andnb, wherenbis a list of[YYYY-MM-DD, count]pairs. - Timestamp lists: either a JSON list of UTC timestamp strings / Unix epoch
seconds, or a plain text file with one timestamp per line. This is useful for
PE
TimeDateStamp/ compiled-timestamp values and other event lists that should be aggregated into weekly patterns.
Run it from the repository:
python -m tempolocus samples/weekfull-chan1.json
python -m tempolocus samples/year.json --format textOr install the package locally:
python -m pip install -e .
tempolocus samples/year-chan1.json --top 10
tempolocus samples/year.json --holiday-profile public-worker --format text
tempolocus samples/year.json --activity-signal peak --format text
tempolocus timestamps.txt --kind timestamps --format text
tempolocus timestamps.txt --start-date 2026-01-01 --end-date 2026-03-31Timestamp strings may use ISO-8601 forms such as 2026-01-05T09:15:00Z
or 2026-01-05 09:15:00 UTC; numeric entries are interpreted as Unix epoch
seconds in UTC. The output is probabilistic JSON and includes a generic analysis.activity_type
classification of work-time, vacation-time, or mixed-time. Weekly inputs
rank timezone offsets, representative IANA zones, and a probable_countries
list that highlights countries whose multiple timezones appear in the top
timezone-offset results. Yearly inputs rank broad regions by comparing
activity on public-holiday calendars.
Large timestamp or yearly inputs can be restricted before analysis with the
inclusive --start-date YYYY-MM-DD and --end-date YYYY-MM-DD options. Either
boundary may be omitted. Date filtering is unavailable for weekly buckets,
because that input shape contains weekdays and hours but no calendar dates.
| Area | Supported regions |
|---|---|
| ๐ Africa | ๐ฉ๐ฟ Algeria, ๐จ๐ฎ Cรดte d'Ivoire, ๐ช๐ฌ Egypt, ๐ช๐น Ethiopia, ๐ฌ๐ญ Ghana, ๐ฐ๐ช Kenya, ๐ฒ๐ฆ Morocco, ๐ณ๐ฌ Nigeria, ๐ธ๐ณ Senegal, ๐ฟ๐ฆ South Africa, ๐น๐ณ Tunisia |
| ๐ Asia-Pacific | ๐ฆ๐บ Australia, ๐จ๐ณ China, ๐ฎ๐ณ India, ๐ฏ๐ต Japan, ๐ฒ๐พ Malaysia, ๐ณ๐ฟ New Zealand, ๐ฐ๐ต North Korea, ๐ต๐ญ Philippines, ๐ธ๐ฌ Singapore, ๐ฐ๐ท South Korea, ๐น๐ญ Thailand, ๐ป๐ณ Vietnam |
| ๐ช๐บ Europe | ๐ฆ๐น Austria, ๐ง๐ช Belgium, ๐ง๐ฆ Bosnia and Herzegovina, ๐ง๐ฌ Bulgaria, ๐ญ๐ท Croatia, ๐จ๐ฟ Czechia, ๐ฉ๐ฐ Denmark, ๐ช๐ช Estonia, ๐ซ๐ฎ Finland, ๐ซ๐ท France, ๐ฉ๐ช Germany, ๐ฌ๐ท Greece, ๐ญ๐บ Hungary, ๐ฎ๐ธ Iceland, ๐ฎ๐ช Ireland, ๐ฎ๐น Italy, ๐ฑ๐ป Latvia, ๐ฑ๐น Lithuania, ๐ฑ๐บ Luxembourg, ๐ณ๐ฑ Netherlands, ๐ณ๐ด Norway, ๐ต๐ฑ Poland, ๐ต๐น Portugal, ๐ท๐ด Romania, ๐ท๐บ Russia, ๐ท๐ธ Serbia, ๐ธ๐ฐ Slovakia, ๐ธ๐ฎ Slovenia, ๐ช๐ธ Spain, ๐ธ๐ช Sweden, ๐จ๐ญ Switzerland, ๐บ๐ฆ Ukraine, ๐ฌ๐ง United Kingdom |
| ๐ North America & Caribbean | ๐จ๐ฆ Canada, ๐จ๐ท Costa Rica, ๐จ๐บ Cuba, ๐ฉ๐ด Dominican Republic, ๐ฌ๐น Guatemala, ๐ฒ๐ฝ Mexico, ๐ต๐ฆ Panama, ๐บ๐ธ United States |
| ๐ Middle East | ๐ง๐ญ Bahrain, ๐ฎ๐ท Iran, ๐ฎ๐ฑ Israel, ๐ฏ๐ด Jordan, ๐ฐ๐ผ Kuwait, ๐ฑ๐ง Lebanon, ๐ด๐ฒ Oman, ๐ถ๐ฆ Qatar, ๐ธ๐ฆ Saudi Arabia, ๐ฆ๐ช United Arab Emirates |
| ๐ South America | ๐ฆ๐ท Argentina, ๐ง๐ท Brazil, ๐จ๐ฑ Chile, ๐จ๐ด Colombia, ๐ต๐ช Peru, ๐บ๐พ Uruguay |
The detailed calendars above cover 80 countries. An additional 117 countries
have fixed-date reference signals, bringing yearly country coverage to
197: the 193 UN members, the Holy See and Palestine, plus Kosovo (XK)
and Taiwan (TW). This is a coverage convention, not a statement on sovereignty.
These additional candidates are explicitly labeled fixed-date references (partial) in JSON and text results. They contain selected national and civic
anniversaries only. They omit movable religious holidays, observed-day shifts,
regional holidays and one-off closures, and do not reconstruct historical
holiday laws. A reference date does not imply every worker has the day off.
The same partial references are available in both holiday profiles; the
public-worker profile does not add government closure rules for them.
Afghanistan (AF), Albania (AL), Andorra (AD), Angola (AO), Antigua and Barbuda (AG), Armenia (AM), Azerbaijan (AZ), Bahamas (BS), Bangladesh (BD), Barbados (BB), Belarus (BY), Belize (BZ), Benin (BJ), Bhutan (BT), Bolivia (BO), Botswana (BW), Brunei (BN), Burkina Faso (BF), Burundi (BI), Cabo Verde (CV), Cambodia (KH), Cameroon (CM), Central African Republic (CF), Chad (TD), Comoros (KM), Republic of the Congo (CG), Democratic Republic of the Congo (CD), Cyprus (CY), Djibouti (DJ), Dominica (DM), Ecuador (EC), El Salvador (SV), Equatorial Guinea (GQ), Eritrea (ER), Eswatini (SZ), Fiji (FJ), Gabon (GA), Gambia (GM), Georgia (GE), Grenada (GD), Guinea (GN), Guinea-Bissau (GW), Guyana (GY), Haiti (HT), Honduras (HN), Indonesia (ID), Iraq (IQ), Jamaica (JM), Kazakhstan (KZ), Kiribati (KI), Kosovo (XK), Kyrgyzstan (KG), Laos (LA), Lesotho (LS), Liberia (LR), Libya (LY), Liechtenstein (LI), Madagascar (MG), Malawi (MW), Maldives (MV), Mali (ML), Malta (MT), Marshall Islands (MH), Mauritania (MR), Mauritius (MU), Micronesia (FM), Moldova (MD), Monaco (MC), Mongolia (MN), Montenegro (ME), Mozambique (MZ), Myanmar (MM), Namibia (NA), Nauru (NR), Nepal (NP), Nicaragua (NI), Niger (NE), North Macedonia (MK), Pakistan (PK), Palau (PW), Palestine (PS), Papua New Guinea (PG), Paraguay (PY), Rwanda (RW), Saint Kitts and Nevis (KN), Saint Lucia (LC), Saint Vincent and the Grenadines (VC), Samoa (WS), San Marino (SM), Sao Tome and Principe (ST), Seychelles (SC), Sierra Leone (SL), Solomon Islands (SB), Somalia (SO), South Sudan (SS), Sri Lanka (LK), Sudan (SD), Suriname (SR), Syria (SY), Taiwan (TW), Tajikistan (TJ), Tanzania (TZ), Timor-Leste (TL), Togo (TG), Tonga (TO), Trinidad and Tobago (TT), Tรผrkiye (TR), Turkmenistan (TM), Tuvalu (TV), Uganda (UG), Uzbekistan (UZ), Vanuatu (VU), Vatican City (VA), Venezuela (VE), Yemen (YE), Zambia (ZM), Zimbabwe (ZW).
Reference dates are maintained in tempolocus/country_references.py;
national days provide a starting
point for checking national anniversaries. Broader country coverage does not
make these sparse references equivalent to the detailed calendars above.
Weekly country inference remains based on matching multiple timezone offsets;
these additions apply to yearly holiday inference.
Yearly analysis treats a lack of activity on holidays as the default signal;
pass --activity-signal peak when unusually high activity is the indicator you
want to match instead. Yearly analysis defaults to public holidays and separately
labeled religious observance references;
pass --holiday-profile public-worker to add public-sector worker references,
such as state-worker, Golden Week, bridge-day, or administrative closure days,
alongside standard holidays. The public-worker profile includes additional
China and Russia references for government and public-sector closure patterns,
and South American public-servant references with common administrative bridge
or year-end closure days.
The calendars include recent statutory changes such as Ireland's Saint Brigid's Day, Poland's Christmas Eve holiday, Italy's Saint Francis of Assisi Day from 2026, and Ukraine's revised holiday dates. Published New Zealand Matariki dates are covered through 2052, while the explicit East Asian lunar reference tables currently cover Chinese New Year and Chuseok through 2035. Islamic dates are calendar estimates and can differ by a day from an official local moon-sighting announcement; regional and one-off government closure announcements should therefore be treated as reference signals rather than payroll calendars.
The generic activity analysis compares weekly business-hours against weekend/off-hours activity, or yearly weekday activity against weekend activity. It is intended as a broad activity-label heuristic rather than a declaration of why the activity occurred.
Both yearly holiday profiles include these candidates automatically. They are holiday reference sets for activity inference, rather than general-purpose calendar conversion APIs.
| Candidate ID | Reference set |
|---|---|
IR |
Iran's combined recurring Solar Hijri and Shia public holidays |
IR-SOLAR |
Solar Hijri holidays: four Nowruz days, Islamic Republic Day, Sizdah Bedar, Khomeini's death anniversary, 15 Khordad, Revolution Day, Oil Nationalization Day, and leap-year end |
IR-SHIA |
Estimated Islamic lunar dates for Tasua, Ashura, Arbaeen, the Prophet and Imams' commemorations, Fatima's martyrdom, Mab'ath, Mahdi's birthday, two Eid al-Fitr days, Eid al-Adha, and Eid al-Ghadir |
IR-ZOROASTRIAN-BASTANI |
Seasonal Iranian reference, using the Solar Hijri New Year model |
IR-ZOROASTRIAN-FASLI |
Seasonal reference with Nowruz fixed to March 21 |
IR-ZOROASTRIAN-QADIMI |
Traditional 365-day calendar, anchored to Nowruz on July 22, 2000 |
IR-ZOROASTRIAN-SHAHANSHAHI |
Traditional 365-day calendar, anchored to Nowruz on August 21, 2000 |
The Zoroastrian sets cover Nowruz, Khordad Sal, Tirgan, Mehregan, Zartosht No-Diso, and the five-day windows of all six Gahambars. These observances are religious references, not nationwide public holidays. Related traditions also exist outside Iran, so a match does not uniquely identify Iran. The labels distinguish the March 21 Fasli convention from the equinox-based Bastani convention; local practice can vary.
Solar Hijri dates use Borkowski's Jalali model, covering Gregorian New Year starts from 560 through 3798. Dates outside that model's range are omitted. The Islamic references use a tabular lunar calendar and explicitly mark every date as estimated; official Iranian moon-sighting dates can differ. The recurring public-holiday rules follow the Iranian holiday calendar; they do not reconstruct past regimes or include one-off closures. Zoroastrian rules follow the calendar variants and festival reference.
This is a heuristic first pass. Weekly data cannot uniquely identify an IANA timezone without dates, and yearly data is sensitive to the meaning of the activity counter.
Install tempolocus in the Python environment that will import it:
python -m pip install tempolocusFor development against a local checkout, install it in editable mode instead:
python -m pip install -e .The public package entry points are detect, analyze_activity, and
load_json:
from tempolocus import analyze_activity, detect, load_json
data = load_json("samples/weekfull-chan1.json")
result = detect(data, top=10)
print(result["input_type"])
print(result["results"][0]["label"])
print(result["analysis"]["activity_type"])
activity = analyze_activity(data)
print(activity["activity_type"])detect(data, kind="auto", top=5, holiday_profile="standard", activity_signal="lack", start_date=None, end_date=None)
Use detect when you want the full inference result. It accepts already-loaded
Python data structures rather than file paths, which makes it suitable for web
services, notebooks, pipelines, and tests. The return value is a dictionary with
metadata, assumptions, signal summaries, and ranked results.
Parameters:
data: one of the supported input shapes described below.kind:"auto","weekly","yearly", or"timestamps". Use"auto"when the input shape is unambiguous; force a kind when your caller already knows what it provided.top: number of ranked candidates to include. Must be at least1.holiday_profile: for yearly inputs,"standard"or"public-worker". The public-worker profile adds public-sector closure references where available.activity_signal: for yearly inputs,"lack"to match low activity on holidays or"peak"to match unusually high activity on holidays.start_dateandend_date: optional inclusiveYYYY-MM-DDstrings (ordatetime.datevalues) used to pre-filter timestamp and yearly inputs before inference. Weekly buckets cannot be date-filtered because they carry no date.
Weekly hourly bucket example:
from tempolocus import detect
weekly_rows = [
{"day": day, "hour": hour, "count": 10 if day <= 4 and 9 <= hour <= 17 else 1}
for day in range(7)
for hour in range(24)
]
result = detect(weekly_rows, kind="weekly", top=3)
for candidate in result["results"]:
print(candidate["probability"], candidate["id"], candidate["label"])Timestamp list example:
from tempolocus import detect
timestamps = [
"2026-01-05T09:15:00Z",
"2026-01-06 10:30:00 UTC",
1767605400, # Unix epoch seconds in UTC
]
result = detect(
timestamps,
kind="timestamps",
start_date="2026-01-01",
end_date="2026-03-31",
)
print(result["signals"]["timestamps_seen"])Yearly daily bucket example:
from tempolocus import detect
yearly = {
"year": 2026,
"max": 42,
"nb": [
["2026-01-01", 0],
["2026-01-02", 18],
["2026-01-03", 21],
],
}
result = detect(
yearly,
kind="yearly",
top=5,
holiday_profile="public-worker",
activity_signal="lack",
)
print(result["results"][0]["id"], result["results"][0]["label"])Use analyze_activity when you only need the generic activity classification
without timezone or holiday-region rankings. It returns fields such as
activity_type, score, and shares. Weekly inputs are classified from local
business-hours versus weekend/off-hours activity; yearly inputs compare weekday
and weekend activity. Its optional date boundaries have the same inclusive
semantics as detect.
from tempolocus import analyze_activity, load_json
activity = analyze_activity(load_json("samples/year.json"), kind="yearly")
print(activity["activity_type"], activity["score"])| Kind | Python shape | Notes |
|---|---|---|
weekly |
list[dict] with day, hour, and count |
day is 0 through 6; hour is 0 through 23; buckets are interpreted as UTC. |
yearly |
dict with year, max, and nb |
nb is a list of [YYYY-MM-DD, count] pairs. Missing days inside the observed range are filled as zero activity. |
timestamps |
list[str | int | float] |
Strings are parsed as UTC timestamps; numbers are Unix epoch seconds in UTC. |
Invalid inputs raise tempolocus.core.DetectionError, a subclass of
ValueError. Catch it around user-supplied data if you need to return a custom
error response:
from tempolocus import detect
from tempolocus.core import DetectionError
try:
result = detect(user_supplied_data)
except DetectionError as error:
result = {"error": str(error)}adulau@blakley:~/git/tempolocus$ python3 -m tempolocus samples/weekfull-chan1.json --format text -n 10 --holiday-profile public-worker
input_type: weekly_timeseries
confidence: 0.220
activity_type: mixed-time (0.009)
assumptions:
- Hourly buckets are interpreted as UTC; timezone candidates are offsets that make the activity look locally human.
- Weekly data cannot distinguish all IANA zones sharing the same offset, and daylight saving time is not inferable without dates.
probable_countries:
0.970 Russia (UTC+02:00, UTC+03:00, UTC+04:00, UTC+05:00, UTC+06:00, UTC+07:00, UTC+08:00)
0.009 France (UTC+01:00, UTC+03:00, UTC+04:00)
0.008 Kazakhstan (UTC+05:00, UTC+06:00)
0.003 United Kingdom (UTC+00:00, UTC+06:00)
0.002 Mongolia (UTC+07:00, UTC+08:00)
results:
0.208 timezone: UTC+05 Pakistan / western Central Asia
utc_quiet_window=19:00-01:00; local_quiet_window=00:00-06:00; quiet_activity_ratio=0.366; local_quiet_center=2.5
0.196 timezone: UTC+04 Gulf / Caucasus
utc_quiet_window=19:00-01:00; local_quiet_window=23:00-05:00; quiet_activity_ratio=0.366; local_quiet_center=1.5
0.143 timezone: UTC+06 Bangladesh / central Asia
utc_quiet_window=19:00-01:00; local_quiet_window=01:00-07:00; quiet_activity_ratio=0.366; local_quiet_center=3.5
0.129 timezone: UTC+03 East Africa / Arabia / Moscow
utc_quiet_window=19:00-01:00; local_quiet_window=22:00-04:00; quiet_activity_ratio=0.366; local_quiet_center=0.5
0.072 timezone: UTC+02 Eastern Europe / southern Africa
utc_quiet_window=19:00-01:00; local_quiet_window=21:00-03:00; quiet_activity_ratio=0.366; local_quiet_center=23.5
0.067 timezone: UTC+07 mainland Southeast Asia
utc_quiet_window=19:00-01:00; local_quiet_window=02:00-08:00; quiet_activity_ratio=0.366; local_quiet_center=4.5
0.039 timezone: UTC+01 Central Europe / West Africa
utc_quiet_window=19:00-01:00; local_quiet_window=20:00-02:00; quiet_activity_ratio=0.366; local_quiet_center=22.5
0.027 timezone: UTC+08 China / Singapore / Western Australia
utc_quiet_window=19:00-01:00; local_quiet_window=03:00-09:00; quiet_activity_ratio=0.366; local_quiet_center=5.5
0.023 timezone: UTC+00 Western Europe / West Africa
utc_quiet_window=19:00-01:00; local_quiet_window=19:00-01:00; quiet_activity_ratio=0.366; local_quiet_center=21.5
0.015 timezone: UTC-01 Azores / Cape Verde
utc_quiet_window=19:00-01:00; local_quiet_window=18:00-00:00; quiet_activity_ratio=0.366; local_quiet_center=20.5
This project is licensed under the GNU Affero General Public License v3.0 or later. See LICENSE for details.