-
- Read the Docs
- v: ${config.versions.current.slug}
-
-
-
-
- ${renderLanguages(config)}
- ${renderVersions(config)}
- ${renderDownloads(config)}
-
- On Read the Docs
-
- Project Home
-
-
- Builds
-
-
- Downloads
-
-
-
- Search
-
-
-
-
-
-
- Hosted by Read the Docs
-
-
-
- `;
-
- // Inject the generated flyout into the body HTML element.
- document.body.insertAdjacentHTML("beforeend", flyout);
-
- // Trigger the Read the Docs Addons Search modal when clicking on the "Search docs" input from inside the flyout.
- document
- .querySelector("#flyout-search-form")
- .addEventListener("focusin", () => {
- const event = new CustomEvent("readthedocs-search-show");
- document.dispatchEvent(event);
- });
- })
-}
-
-if (themeLanguageSelector || themeVersionSelector) {
- function onSelectorSwitch(event) {
- const option = event.target.selectedIndex;
- const item = event.target.options[option];
- window.location.href = item.dataset.url;
- }
-
- document.addEventListener("readthedocs-addons-data-ready", function (event) {
- const config = event.detail.data();
-
- const versionSwitch = document.querySelector(
- "div.switch-menus > div.version-switch",
- );
- if (themeVersionSelector) {
- let versions = config.versions.active;
- if (config.versions.current.hidden || config.versions.current.type === "external") {
- versions.unshift(config.versions.current);
- }
- const versionSelect = `
-
- ${versions
- .map(
- (version) => `
-
- ${version.slug}
- `,
- )
- .join("\n")}
-
- `;
-
- versionSwitch.innerHTML = versionSelect;
- versionSwitch.firstElementChild.addEventListener("change", onSelectorSwitch);
- }
-
- const languageSwitch = document.querySelector(
- "div.switch-menus > div.language-switch",
- );
-
- if (themeLanguageSelector) {
- if (config.projects.translations.length) {
- // Add the current language to the options on the selector
- let languages = config.projects.translations.concat(
- config.projects.current,
- );
- languages = languages.sort((a, b) =>
- a.language.name.localeCompare(b.language.name),
- );
-
- const languageSelect = `
-
- ${languages
- .map(
- (language) => `
-
- ${language.language.name}
- `,
- )
- .join("\n")}
-
- `;
-
- languageSwitch.innerHTML = languageSelect;
- languageSwitch.firstElementChild.addEventListener("change", onSelectorSwitch);
- }
- else {
- languageSwitch.remove();
- }
- }
- });
-}
-
-document.addEventListener("readthedocs-addons-data-ready", function (event) {
- // Trigger the Read the Docs Addons Search modal when clicking on "Search docs" input from the topnav.
- document
- .querySelector("[role='search'] input")
- .addEventListener("focusin", () => {
- const event = new CustomEvent("readthedocs-search-show");
- document.dispatchEvent(event);
- });
-});
\ No newline at end of file
diff --git a/docs/_build/html/_static/language_data.js b/docs/_build/html/_static/language_data.js
deleted file mode 100644
index c7fe6c6f..00000000
--- a/docs/_build/html/_static/language_data.js
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * This script contains the language-specific data used by searchtools.js,
- * namely the list of stopwords, stemmer, scorer and splitter.
- */
-
-var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"];
-
-
-/* Non-minified version is copied as a separate JS file, if available */
-
-/**
- * Porter Stemmer
- */
-var Stemmer = function() {
-
- var step2list = {
- ational: 'ate',
- tional: 'tion',
- enci: 'ence',
- anci: 'ance',
- izer: 'ize',
- bli: 'ble',
- alli: 'al',
- entli: 'ent',
- eli: 'e',
- ousli: 'ous',
- ization: 'ize',
- ation: 'ate',
- ator: 'ate',
- alism: 'al',
- iveness: 'ive',
- fulness: 'ful',
- ousness: 'ous',
- aliti: 'al',
- iviti: 'ive',
- biliti: 'ble',
- logi: 'log'
- };
-
- var step3list = {
- icate: 'ic',
- ative: '',
- alize: 'al',
- iciti: 'ic',
- ical: 'ic',
- ful: '',
- ness: ''
- };
-
- var c = "[^aeiou]"; // consonant
- var v = "[aeiouy]"; // vowel
- var C = c + "[^aeiouy]*"; // consonant sequence
- var V = v + "[aeiou]*"; // vowel sequence
-
- var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0
- var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1
- var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1
- var s_v = "^(" + C + ")?" + v; // vowel in stem
-
- this.stemWord = function (w) {
- var stem;
- var suffix;
- var firstch;
- var origword = w;
-
- if (w.length < 3)
- return w;
-
- var re;
- var re2;
- var re3;
- var re4;
-
- firstch = w.substr(0,1);
- if (firstch == "y")
- w = firstch.toUpperCase() + w.substr(1);
-
- // Step 1a
- re = /^(.+?)(ss|i)es$/;
- re2 = /^(.+?)([^s])s$/;
-
- if (re.test(w))
- w = w.replace(re,"$1$2");
- else if (re2.test(w))
- w = w.replace(re2,"$1$2");
-
- // Step 1b
- re = /^(.+?)eed$/;
- re2 = /^(.+?)(ed|ing)$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- re = new RegExp(mgr0);
- if (re.test(fp[1])) {
- re = /.$/;
- w = w.replace(re,"");
- }
- }
- else if (re2.test(w)) {
- var fp = re2.exec(w);
- stem = fp[1];
- re2 = new RegExp(s_v);
- if (re2.test(stem)) {
- w = stem;
- re2 = /(at|bl|iz)$/;
- re3 = new RegExp("([^aeiouylsz])\\1$");
- re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
- if (re2.test(w))
- w = w + "e";
- else if (re3.test(w)) {
- re = /.$/;
- w = w.replace(re,"");
- }
- else if (re4.test(w))
- w = w + "e";
- }
- }
-
- // Step 1c
- re = /^(.+?)y$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- stem = fp[1];
- re = new RegExp(s_v);
- if (re.test(stem))
- w = stem + "i";
- }
-
- // Step 2
- re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- stem = fp[1];
- suffix = fp[2];
- re = new RegExp(mgr0);
- if (re.test(stem))
- w = stem + step2list[suffix];
- }
-
- // Step 3
- re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- stem = fp[1];
- suffix = fp[2];
- re = new RegExp(mgr0);
- if (re.test(stem))
- w = stem + step3list[suffix];
- }
-
- // Step 4
- re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
- re2 = /^(.+?)(s|t)(ion)$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- stem = fp[1];
- re = new RegExp(mgr1);
- if (re.test(stem))
- w = stem;
- }
- else if (re2.test(w)) {
- var fp = re2.exec(w);
- stem = fp[1] + fp[2];
- re2 = new RegExp(mgr1);
- if (re2.test(stem))
- w = stem;
- }
-
- // Step 5
- re = /^(.+?)e$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- stem = fp[1];
- re = new RegExp(mgr1);
- re2 = new RegExp(meq1);
- re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
- if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))
- w = stem;
- }
- re = /ll$/;
- re2 = new RegExp(mgr1);
- if (re.test(w) && re2.test(w)) {
- re = /.$/;
- w = w.replace(re,"");
- }
-
- // and turn initial Y back to y
- if (firstch == "y")
- w = firstch.toLowerCase() + w.substr(1);
- return w;
- }
-}
-
diff --git a/docs/_build/html/_static/minus.png b/docs/_build/html/_static/minus.png
deleted file mode 100644
index d96755fd..00000000
Binary files a/docs/_build/html/_static/minus.png and /dev/null differ
diff --git a/docs/_build/html/_static/plus.png b/docs/_build/html/_static/plus.png
deleted file mode 100644
index 7107cec9..00000000
Binary files a/docs/_build/html/_static/plus.png and /dev/null differ
diff --git a/docs/_build/html/_static/pygments.css b/docs/_build/html/_static/pygments.css
deleted file mode 100644
index 84ab3030..00000000
--- a/docs/_build/html/_static/pygments.css
+++ /dev/null
@@ -1,75 +0,0 @@
-pre { line-height: 125%; }
-td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
-span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
-td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
-span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
-.highlight .hll { background-color: #ffffcc }
-.highlight { background: #f8f8f8; }
-.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */
-.highlight .err { border: 1px solid #FF0000 } /* Error */
-.highlight .k { color: #008000; font-weight: bold } /* Keyword */
-.highlight .o { color: #666666 } /* Operator */
-.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
-.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
-.highlight .cp { color: #9C6500 } /* Comment.Preproc */
-.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
-.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
-.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
-.highlight .gd { color: #A00000 } /* Generic.Deleted */
-.highlight .ge { font-style: italic } /* Generic.Emph */
-.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
-.highlight .gr { color: #E40000 } /* Generic.Error */
-.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.highlight .gi { color: #008400 } /* Generic.Inserted */
-.highlight .go { color: #717171 } /* Generic.Output */
-.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
-.highlight .gs { font-weight: bold } /* Generic.Strong */
-.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
-.highlight .gt { color: #0044DD } /* Generic.Traceback */
-.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
-.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
-.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
-.highlight .kp { color: #008000 } /* Keyword.Pseudo */
-.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
-.highlight .kt { color: #B00040 } /* Keyword.Type */
-.highlight .m { color: #666666 } /* Literal.Number */
-.highlight .s { color: #BA2121 } /* Literal.String */
-.highlight .na { color: #687822 } /* Name.Attribute */
-.highlight .nb { color: #008000 } /* Name.Builtin */
-.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
-.highlight .no { color: #880000 } /* Name.Constant */
-.highlight .nd { color: #AA22FF } /* Name.Decorator */
-.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */
-.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
-.highlight .nf { color: #0000FF } /* Name.Function */
-.highlight .nl { color: #767600 } /* Name.Label */
-.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
-.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
-.highlight .nv { color: #19177C } /* Name.Variable */
-.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
-.highlight .w { color: #bbbbbb } /* Text.Whitespace */
-.highlight .mb { color: #666666 } /* Literal.Number.Bin */
-.highlight .mf { color: #666666 } /* Literal.Number.Float */
-.highlight .mh { color: #666666 } /* Literal.Number.Hex */
-.highlight .mi { color: #666666 } /* Literal.Number.Integer */
-.highlight .mo { color: #666666 } /* Literal.Number.Oct */
-.highlight .sa { color: #BA2121 } /* Literal.String.Affix */
-.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */
-.highlight .sc { color: #BA2121 } /* Literal.String.Char */
-.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */
-.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
-.highlight .s2 { color: #BA2121 } /* Literal.String.Double */
-.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
-.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */
-.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
-.highlight .sx { color: #008000 } /* Literal.String.Other */
-.highlight .sr { color: #A45A77 } /* Literal.String.Regex */
-.highlight .s1 { color: #BA2121 } /* Literal.String.Single */
-.highlight .ss { color: #19177C } /* Literal.String.Symbol */
-.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
-.highlight .fm { color: #0000FF } /* Name.Function.Magic */
-.highlight .vc { color: #19177C } /* Name.Variable.Class */
-.highlight .vg { color: #19177C } /* Name.Variable.Global */
-.highlight .vi { color: #19177C } /* Name.Variable.Instance */
-.highlight .vm { color: #19177C } /* Name.Variable.Magic */
-.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
\ No newline at end of file
diff --git a/docs/_build/html/_static/searchtools.js b/docs/_build/html/_static/searchtools.js
deleted file mode 100644
index 2c774d17..00000000
--- a/docs/_build/html/_static/searchtools.js
+++ /dev/null
@@ -1,632 +0,0 @@
-/*
- * Sphinx JavaScript utilities for the full-text search.
- */
-"use strict";
-
-/**
- * Simple result scoring code.
- */
-if (typeof Scorer === "undefined") {
- var Scorer = {
- // Implement the following function to further tweak the score for each result
- // The function takes a result array [docname, title, anchor, descr, score, filename]
- // and returns the new score.
- /*
- score: result => {
- const [docname, title, anchor, descr, score, filename, kind] = result
- return score
- },
- */
-
- // query matches the full name of an object
- objNameMatch: 11,
- // or matches in the last dotted part of the object name
- objPartialMatch: 6,
- // Additive scores depending on the priority of the object
- objPrio: {
- 0: 15, // used to be importantResults
- 1: 5, // used to be objectResults
- 2: -5, // used to be unimportantResults
- },
- // Used when the priority is not in the mapping.
- objPrioDefault: 0,
-
- // query found in title
- title: 15,
- partialTitle: 7,
- // query found in terms
- term: 5,
- partialTerm: 2,
- };
-}
-
-// Global search result kind enum, used by themes to style search results.
-class SearchResultKind {
- static get index() { return "index"; }
- static get object() { return "object"; }
- static get text() { return "text"; }
- static get title() { return "title"; }
-}
-
-const _removeChildren = (element) => {
- while (element && element.lastChild) element.removeChild(element.lastChild);
-};
-
-/**
- * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
- */
-const _escapeRegExp = (string) =>
- string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
-
-const _displayItem = (item, searchTerms, highlightTerms) => {
- const docBuilder = DOCUMENTATION_OPTIONS.BUILDER;
- const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;
- const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;
- const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;
- const contentRoot = document.documentElement.dataset.content_root;
-
- const [docName, title, anchor, descr, score, _filename, kind] = item;
-
- let listItem = document.createElement("li");
- // Add a class representing the item's type:
- // can be used by a theme's CSS selector for styling
- // See SearchResultKind for the class names.
- listItem.classList.add(`kind-${kind}`);
- let requestUrl;
- let linkUrl;
- if (docBuilder === "dirhtml") {
- // dirhtml builder
- let dirname = docName + "/";
- if (dirname.match(/\/index\/$/))
- dirname = dirname.substring(0, dirname.length - 6);
- else if (dirname === "index/") dirname = "";
- requestUrl = contentRoot + dirname;
- linkUrl = requestUrl;
- } else {
- // normal html builders
- requestUrl = contentRoot + docName + docFileSuffix;
- linkUrl = docName + docLinkSuffix;
- }
- let linkEl = listItem.appendChild(document.createElement("a"));
- linkEl.href = linkUrl + anchor;
- linkEl.dataset.score = score;
- linkEl.innerHTML = title;
- if (descr) {
- listItem.appendChild(document.createElement("span")).innerHTML =
- " (" + descr + ")";
- // highlight search terms in the description
- if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js
- highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted"));
- }
- else if (showSearchSummary)
- fetch(requestUrl)
- .then((responseData) => responseData.text())
- .then((data) => {
- if (data)
- listItem.appendChild(
- Search.makeSearchSummary(data, searchTerms, anchor)
- );
- // highlight search terms in the summary
- if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js
- highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted"));
- });
- Search.output.appendChild(listItem);
-};
-const _finishSearch = (resultCount) => {
- Search.stopPulse();
- Search.title.innerText = _("Search Results");
- if (!resultCount)
- Search.status.innerText = Documentation.gettext(
- "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories."
- );
- else
- Search.status.innerText = Documentation.ngettext(
- "Search finished, found one page matching the search query.",
- "Search finished, found ${resultCount} pages matching the search query.",
- resultCount,
- ).replace('${resultCount}', resultCount);
-};
-const _displayNextItem = (
- results,
- resultCount,
- searchTerms,
- highlightTerms,
-) => {
- // results left, load the summary and display it
- // this is intended to be dynamic (don't sub resultsCount)
- if (results.length) {
- _displayItem(results.pop(), searchTerms, highlightTerms);
- setTimeout(
- () => _displayNextItem(results, resultCount, searchTerms, highlightTerms),
- 5
- );
- }
- // search finished, update title and status message
- else _finishSearch(resultCount);
-};
-// Helper function used by query() to order search results.
-// Each input is an array of [docname, title, anchor, descr, score, filename, kind].
-// Order the results by score (in opposite order of appearance, since the
-// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically.
-const _orderResultsByScoreThenName = (a, b) => {
- const leftScore = a[4];
- const rightScore = b[4];
- if (leftScore === rightScore) {
- // same score: sort alphabetically
- const leftTitle = a[1].toLowerCase();
- const rightTitle = b[1].toLowerCase();
- if (leftTitle === rightTitle) return 0;
- return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
- }
- return leftScore > rightScore ? 1 : -1;
-};
-
-/**
- * Default splitQuery function. Can be overridden in ``sphinx.search`` with a
- * custom function per language.
- *
- * The regular expression works by splitting the string on consecutive characters
- * that are not Unicode letters, numbers, underscores, or emoji characters.
- * This is the same as ``\W+`` in Python, preserving the surrogate pair area.
- */
-if (typeof splitQuery === "undefined") {
- var splitQuery = (query) => query
- .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu)
- .filter(term => term) // remove remaining empty strings
-}
-
-/**
- * Search Module
- */
-const Search = {
- _index: null,
- _queued_query: null,
- _pulse_status: -1,
-
- htmlToText: (htmlString, anchor) => {
- const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html');
- for (const removalQuery of [".headerlink", "script", "style"]) {
- htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() });
- }
- if (anchor) {
- const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`);
- if (anchorContent) return anchorContent.textContent;
-
- console.warn(
- `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`
- );
- }
-
- // if anchor not specified or not found, fall back to main content
- const docContent = htmlElement.querySelector('[role="main"]');
- if (docContent) return docContent.textContent;
-
- console.warn(
- "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template."
- );
- return "";
- },
-
- init: () => {
- const query = new URLSearchParams(window.location.search).get("q");
- document
- .querySelectorAll('input[name="q"]')
- .forEach((el) => (el.value = query));
- if (query) Search.performSearch(query);
- },
-
- loadIndex: (url) =>
- (document.body.appendChild(document.createElement("script")).src = url),
-
- setIndex: (index) => {
- Search._index = index;
- if (Search._queued_query !== null) {
- const query = Search._queued_query;
- Search._queued_query = null;
- Search.query(query);
- }
- },
-
- hasIndex: () => Search._index !== null,
-
- deferQuery: (query) => (Search._queued_query = query),
-
- stopPulse: () => (Search._pulse_status = -1),
-
- startPulse: () => {
- if (Search._pulse_status >= 0) return;
-
- const pulse = () => {
- Search._pulse_status = (Search._pulse_status + 1) % 4;
- Search.dots.innerText = ".".repeat(Search._pulse_status);
- if (Search._pulse_status >= 0) window.setTimeout(pulse, 500);
- };
- pulse();
- },
-
- /**
- * perform a search for something (or wait until index is loaded)
- */
- performSearch: (query) => {
- // create the required interface elements
- const searchText = document.createElement("h2");
- searchText.textContent = _("Searching");
- const searchSummary = document.createElement("p");
- searchSummary.classList.add("search-summary");
- searchSummary.innerText = "";
- const searchList = document.createElement("ul");
- searchList.setAttribute("role", "list");
- searchList.classList.add("search");
-
- const out = document.getElementById("search-results");
- Search.title = out.appendChild(searchText);
- Search.dots = Search.title.appendChild(document.createElement("span"));
- Search.status = out.appendChild(searchSummary);
- Search.output = out.appendChild(searchList);
-
- const searchProgress = document.getElementById("search-progress");
- // Some themes don't use the search progress node
- if (searchProgress) {
- searchProgress.innerText = _("Preparing search...");
- }
- Search.startPulse();
-
- // index already loaded, the browser was quick!
- if (Search.hasIndex()) Search.query(query);
- else Search.deferQuery(query);
- },
-
- _parseQuery: (query) => {
- // stem the search terms and add them to the correct list
- const stemmer = new Stemmer();
- const searchTerms = new Set();
- const excludedTerms = new Set();
- const highlightTerms = new Set();
- const objectTerms = new Set(splitQuery(query.toLowerCase().trim()));
- splitQuery(query.trim()).forEach((queryTerm) => {
- const queryTermLower = queryTerm.toLowerCase();
-
- // maybe skip this "word"
- // stopwords array is from language_data.js
- if (
- stopwords.indexOf(queryTermLower) !== -1 ||
- queryTerm.match(/^\d+$/)
- )
- return;
-
- // stem the word
- let word = stemmer.stemWord(queryTermLower);
- // select the correct list
- if (word[0] === "-") excludedTerms.add(word.substr(1));
- else {
- searchTerms.add(word);
- highlightTerms.add(queryTermLower);
- }
- });
-
- if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js
- localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" "))
- }
-
- // console.debug("SEARCH: searching for:");
- // console.info("required: ", [...searchTerms]);
- // console.info("excluded: ", [...excludedTerms]);
-
- return [query, searchTerms, excludedTerms, highlightTerms, objectTerms];
- },
-
- /**
- * execute search (requires search index to be loaded)
- */
- _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => {
- const filenames = Search._index.filenames;
- const docNames = Search._index.docnames;
- const titles = Search._index.titles;
- const allTitles = Search._index.alltitles;
- const indexEntries = Search._index.indexentries;
-
- // Collect multiple result groups to be sorted separately and then ordered.
- // Each is an array of [docname, title, anchor, descr, score, filename, kind].
- const normalResults = [];
- const nonMainIndexResults = [];
-
- _removeChildren(document.getElementById("search-progress"));
-
- const queryLower = query.toLowerCase().trim();
- for (const [title, foundTitles] of Object.entries(allTitles)) {
- if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) {
- for (const [file, id] of foundTitles) {
- const score = Math.round(Scorer.title * queryLower.length / title.length);
- const boost = titles[file] === title ? 1 : 0; // add a boost for document titles
- normalResults.push([
- docNames[file],
- titles[file] !== title ? `${titles[file]} > ${title}` : title,
- id !== null ? "#" + id : "",
- null,
- score + boost,
- filenames[file],
- SearchResultKind.title,
- ]);
- }
- }
- }
-
- // search for explicit entries in index directives
- for (const [entry, foundEntries] of Object.entries(indexEntries)) {
- if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) {
- for (const [file, id, isMain] of foundEntries) {
- const score = Math.round(100 * queryLower.length / entry.length);
- const result = [
- docNames[file],
- titles[file],
- id ? "#" + id : "",
- null,
- score,
- filenames[file],
- SearchResultKind.index,
- ];
- if (isMain) {
- normalResults.push(result);
- } else {
- nonMainIndexResults.push(result);
- }
- }
- }
- }
-
- // lookup as object
- objectTerms.forEach((term) =>
- normalResults.push(...Search.performObjectSearch(term, objectTerms))
- );
-
- // lookup as search terms in fulltext
- normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms));
-
- // let the scorer override scores with a custom scoring function
- if (Scorer.score) {
- normalResults.forEach((item) => (item[4] = Scorer.score(item)));
- nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item)));
- }
-
- // Sort each group of results by score and then alphabetically by name.
- normalResults.sort(_orderResultsByScoreThenName);
- nonMainIndexResults.sort(_orderResultsByScoreThenName);
-
- // Combine the result groups in (reverse) order.
- // Non-main index entries are typically arbitrary cross-references,
- // so display them after other results.
- let results = [...nonMainIndexResults, ...normalResults];
-
- // remove duplicate search results
- // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
- let seen = new Set();
- results = results.reverse().reduce((acc, result) => {
- let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(',');
- if (!seen.has(resultStr)) {
- acc.push(result);
- seen.add(resultStr);
- }
- return acc;
- }, []);
-
- return results.reverse();
- },
-
- query: (query) => {
- const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query);
- const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms);
-
- // for debugging
- //Search.lastresults = results.slice(); // a copy
- // console.info("search results:", Search.lastresults);
-
- // print the results
- _displayNextItem(results, results.length, searchTerms, highlightTerms);
- },
-
- /**
- * search for object names
- */
- performObjectSearch: (object, objectTerms) => {
- const filenames = Search._index.filenames;
- const docNames = Search._index.docnames;
- const objects = Search._index.objects;
- const objNames = Search._index.objnames;
- const titles = Search._index.titles;
-
- const results = [];
-
- const objectSearchCallback = (prefix, match) => {
- const name = match[4]
- const fullname = (prefix ? prefix + "." : "") + name;
- const fullnameLower = fullname.toLowerCase();
- if (fullnameLower.indexOf(object) < 0) return;
-
- let score = 0;
- const parts = fullnameLower.split(".");
-
- // check for different match types: exact matches of full name or
- // "last name" (i.e. last dotted part)
- if (fullnameLower === object || parts.slice(-1)[0] === object)
- score += Scorer.objNameMatch;
- else if (parts.slice(-1)[0].indexOf(object) > -1)
- score += Scorer.objPartialMatch; // matches in last name
-
- const objName = objNames[match[1]][2];
- const title = titles[match[0]];
-
- // If more than one term searched for, we require other words to be
- // found in the name/title/description
- const otherTerms = new Set(objectTerms);
- otherTerms.delete(object);
- if (otherTerms.size > 0) {
- const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase();
- if (
- [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0)
- )
- return;
- }
-
- let anchor = match[3];
- if (anchor === "") anchor = fullname;
- else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname;
-
- const descr = objName + _(", in ") + title;
-
- // add custom score for some objects according to scorer
- if (Scorer.objPrio.hasOwnProperty(match[2]))
- score += Scorer.objPrio[match[2]];
- else score += Scorer.objPrioDefault;
-
- results.push([
- docNames[match[0]],
- fullname,
- "#" + anchor,
- descr,
- score,
- filenames[match[0]],
- SearchResultKind.object,
- ]);
- };
- Object.keys(objects).forEach((prefix) =>
- objects[prefix].forEach((array) =>
- objectSearchCallback(prefix, array)
- )
- );
- return results;
- },
-
- /**
- * search for full-text terms in the index
- */
- performTermsSearch: (searchTerms, excludedTerms) => {
- // prepare search
- const terms = Search._index.terms;
- const titleTerms = Search._index.titleterms;
- const filenames = Search._index.filenames;
- const docNames = Search._index.docnames;
- const titles = Search._index.titles;
-
- const scoreMap = new Map();
- const fileMap = new Map();
-
- // perform the search on the required terms
- searchTerms.forEach((word) => {
- const files = [];
- const arr = [
- { files: terms[word], score: Scorer.term },
- { files: titleTerms[word], score: Scorer.title },
- ];
- // add support for partial matches
- if (word.length > 2) {
- const escapedWord = _escapeRegExp(word);
- if (!terms.hasOwnProperty(word)) {
- Object.keys(terms).forEach((term) => {
- if (term.match(escapedWord))
- arr.push({ files: terms[term], score: Scorer.partialTerm });
- });
- }
- if (!titleTerms.hasOwnProperty(word)) {
- Object.keys(titleTerms).forEach((term) => {
- if (term.match(escapedWord))
- arr.push({ files: titleTerms[term], score: Scorer.partialTitle });
- });
- }
- }
-
- // no match but word was a required one
- if (arr.every((record) => record.files === undefined)) return;
-
- // found search word in contents
- arr.forEach((record) => {
- if (record.files === undefined) return;
-
- let recordFiles = record.files;
- if (recordFiles.length === undefined) recordFiles = [recordFiles];
- files.push(...recordFiles);
-
- // set score for the word in each file
- recordFiles.forEach((file) => {
- if (!scoreMap.has(file)) scoreMap.set(file, {});
- scoreMap.get(file)[word] = record.score;
- });
- });
-
- // create the mapping
- files.forEach((file) => {
- if (!fileMap.has(file)) fileMap.set(file, [word]);
- else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word);
- });
- });
-
- // now check if the files don't contain excluded terms
- const results = [];
- for (const [file, wordList] of fileMap) {
- // check if all requirements are matched
-
- // as search terms with length < 3 are discarded
- const filteredTermCount = [...searchTerms].filter(
- (term) => term.length > 2
- ).length;
- if (
- wordList.length !== searchTerms.size &&
- wordList.length !== filteredTermCount
- )
- continue;
-
- // ensure that none of the excluded terms is in the search result
- if (
- [...excludedTerms].some(
- (term) =>
- terms[term] === file ||
- titleTerms[term] === file ||
- (terms[term] || []).includes(file) ||
- (titleTerms[term] || []).includes(file)
- )
- )
- break;
-
- // select one (max) score for the file.
- const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w]));
- // add result to the result list
- results.push([
- docNames[file],
- titles[file],
- "",
- null,
- score,
- filenames[file],
- SearchResultKind.text,
- ]);
- }
- return results;
- },
-
- /**
- * helper function to return a node containing the
- * search summary for a given text. keywords is a list
- * of stemmed words.
- */
- makeSearchSummary: (htmlText, keywords, anchor) => {
- const text = Search.htmlToText(htmlText, anchor);
- if (text === "") return null;
-
- const textLower = text.toLowerCase();
- const actualStartPosition = [...keywords]
- .map((k) => textLower.indexOf(k.toLowerCase()))
- .filter((i) => i > -1)
- .slice(-1)[0];
- const startWithContext = Math.max(actualStartPosition - 120, 0);
-
- const top = startWithContext === 0 ? "" : "...";
- const tail = startWithContext + 240 < text.length ? "..." : "";
-
- let summary = document.createElement("p");
- summary.classList.add("context");
- summary.textContent = top + text.substr(startWithContext, 240).trim() + tail;
-
- return summary;
- },
-};
-
-_ready(Search.init);
diff --git a/docs/_build/html/_static/sphinx_highlight.js b/docs/_build/html/_static/sphinx_highlight.js
deleted file mode 100644
index 8a96c69a..00000000
--- a/docs/_build/html/_static/sphinx_highlight.js
+++ /dev/null
@@ -1,154 +0,0 @@
-/* Highlighting utilities for Sphinx HTML documentation. */
-"use strict";
-
-const SPHINX_HIGHLIGHT_ENABLED = true
-
-/**
- * highlight a given string on a node by wrapping it in
- * span elements with the given class name.
- */
-const _highlight = (node, addItems, text, className) => {
- if (node.nodeType === Node.TEXT_NODE) {
- const val = node.nodeValue;
- const parent = node.parentNode;
- const pos = val.toLowerCase().indexOf(text);
- if (
- pos >= 0 &&
- !parent.classList.contains(className) &&
- !parent.classList.contains("nohighlight")
- ) {
- let span;
-
- const closestNode = parent.closest("body, svg, foreignObject");
- const isInSVG = closestNode && closestNode.matches("svg");
- if (isInSVG) {
- span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
- } else {
- span = document.createElement("span");
- span.classList.add(className);
- }
-
- span.appendChild(document.createTextNode(val.substr(pos, text.length)));
- const rest = document.createTextNode(val.substr(pos + text.length));
- parent.insertBefore(
- span,
- parent.insertBefore(
- rest,
- node.nextSibling
- )
- );
- node.nodeValue = val.substr(0, pos);
- /* There may be more occurrences of search term in this node. So call this
- * function recursively on the remaining fragment.
- */
- _highlight(rest, addItems, text, className);
-
- if (isInSVG) {
- const rect = document.createElementNS(
- "http://www.w3.org/2000/svg",
- "rect"
- );
- const bbox = parent.getBBox();
- rect.x.baseVal.value = bbox.x;
- rect.y.baseVal.value = bbox.y;
- rect.width.baseVal.value = bbox.width;
- rect.height.baseVal.value = bbox.height;
- rect.setAttribute("class", className);
- addItems.push({ parent: parent, target: rect });
- }
- }
- } else if (node.matches && !node.matches("button, select, textarea")) {
- node.childNodes.forEach((el) => _highlight(el, addItems, text, className));
- }
-};
-const _highlightText = (thisNode, text, className) => {
- let addItems = [];
- _highlight(thisNode, addItems, text, className);
- addItems.forEach((obj) =>
- obj.parent.insertAdjacentElement("beforebegin", obj.target)
- );
-};
-
-/**
- * Small JavaScript module for the documentation.
- */
-const SphinxHighlight = {
-
- /**
- * highlight the search words provided in localstorage in the text
- */
- highlightSearchWords: () => {
- if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight
-
- // get and clear terms from localstorage
- const url = new URL(window.location);
- const highlight =
- localStorage.getItem("sphinx_highlight_terms")
- || url.searchParams.get("highlight")
- || "";
- localStorage.removeItem("sphinx_highlight_terms")
- url.searchParams.delete("highlight");
- window.history.replaceState({}, "", url);
-
- // get individual terms from highlight string
- const terms = highlight.toLowerCase().split(/\s+/).filter(x => x);
- if (terms.length === 0) return; // nothing to do
-
- // There should never be more than one element matching "div.body"
- const divBody = document.querySelectorAll("div.body");
- const body = divBody.length ? divBody[0] : document.querySelector("body");
- window.setTimeout(() => {
- terms.forEach((term) => _highlightText(body, term, "highlighted"));
- }, 10);
-
- const searchBox = document.getElementById("searchbox");
- if (searchBox === null) return;
- searchBox.appendChild(
- document
- .createRange()
- .createContextualFragment(
- '
' +
- '' +
- _("Hide Search Matches") +
- "
"
- )
- );
- },
-
- /**
- * helper function to hide the search marks again
- */
- hideSearchWords: () => {
- document
- .querySelectorAll("#searchbox .highlight-link")
- .forEach((el) => el.remove());
- document
- .querySelectorAll("span.highlighted")
- .forEach((el) => el.classList.remove("highlighted"));
- localStorage.removeItem("sphinx_highlight_terms")
- },
-
- initEscapeListener: () => {
- // only install a listener if it is really needed
- if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return;
-
- document.addEventListener("keydown", (event) => {
- // bail for input elements
- if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return;
- // bail with special keys
- if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
- if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) {
- SphinxHighlight.hideSearchWords();
- event.preventDefault();
- }
- });
- },
-};
-
-_ready(() => {
- /* Do not call highlightSearchWords() when we are on the search page.
- * It will highlight words from the *previous* search query.
- */
- if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords();
- SphinxHighlight.initEscapeListener();
-});
diff --git a/docs/_build/html/base_module.html b/docs/_build/html/base_module.html
deleted file mode 100644
index d6959ffc..00000000
--- a/docs/_build/html/base_module.html
+++ /dev/null
@@ -1,141 +0,0 @@
-
-
-
-
-
-
-
-
-
base_module package — NOMAD 0.0.1 documentation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- NOMAD
-
-
-
-
-
-
-
-
-
-base_module package
-
-
-base_module.doc_test module
-
-
-base_module.setup module
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/_build/html/genindex.html b/docs/_build/html/genindex.html
deleted file mode 100644
index 9f4b7ef5..00000000
--- a/docs/_build/html/genindex.html
+++ /dev/null
@@ -1,649 +0,0 @@
-
-
-
-
-
-
-
-
Index — NOMAD 0.0.1 documentation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- NOMAD
-
-
-
-
-
-
-
-
-
-
Index
-
-
-
_
- |
A
- |
B
- |
C
- |
D
- |
F
- |
G
- |
I
- |
L
- |
M
- |
N
- |
O
- |
P
- |
Q
- |
R
- |
S
- |
T
- |
W
-
-
-
_
-
-
-
A
-
-
-
B
-
-
-
C
-
-
-
D
-
-
-
F
-
-
-
G
-
-
-
I
-
-
-
L
-
-
-
M
-
-
-
N
-
-
- nocturnal_stops() (in module nomad.home_attribution)
-
-
- nomad.agg
-
-
-
- nomad.aggregation
-
-
-
- nomad.city_gen
-
-
-
- nomad.constants
-
-
-
- nomad.contact_estimation
-
-
-
- nomad.displacement
-
-
-
- nomad.filters
-
-
-
- nomad.filters_spark
-
-
-
- nomad.generation
-
-
-
-
-
-
-
O
-
-
-
P
-
-
-
Q
-
-
-
R
-
-
-
S
-
-
-
T
-
-
-
W
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/_build/html/index.html b/docs/_build/html/index.html
deleted file mode 100644
index fa2304b2..00000000
--- a/docs/_build/html/index.html
+++ /dev/null
@@ -1,225 +0,0 @@
-
-
-
-
-
-
-
-
-
NOMAD: Network for Open Mobility Analysis and Data — NOMAD 0.0.1 documentation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- NOMAD
-
-
-
-
-
-
-
-
-
-NOMAD: Network for Open Mobility Analysis and Data
-
-
-
-
-NOMAD is an open-source Python library for end-to-end processing of large-scale GPS mobility data.
-Part of the NSF-funded NOMAD research infrastructure project, it provides production-ready tools for
-mobility data analysis with seamless scaling from local workstations to Spark clusters.
-NOMAD builds on previous software resources—like scikit-mobility, mobilkit, and trackintel—with the
-goal of providing a single, production-ready library that covers the entire processing pipeline in a
-form suitable for analysis of massive datasets and to aid in the replicability of existing research.
-All functions are implemented in Python with parallel equivalents in PySpark, enabling the same
-analysis notebook to run on a workstation or a Spark cluster without API changes.
-
-
-Installation
- pip install git+https://github.com/Watts-Lab/nomad.git
-
-
-
-
-
-
-
-
-
-
-Modules Overview
-
-
-
-
-
-
-Module
-Description
-
-
-
-Data Ingestion
-Read CSV, Parquet, GeoJSON; validate schemas; return Pandas or Spark DataFrames
-
-Filtering & Completeness
-Assess coverage; filter by completeness, geography, time; handle projections
-
-Tessellation
-Map pings to H3, S2, or custom grids for grid-based algorithms
-
-Stop Detection
-DBSCAN, HDBSCAN, grid-based, and sequential (Lachesis) algorithms
-
-Visit Attribution
-Frequency and time-window heuristics for home/workplace inference
-
-Mobility Metrics
-Radius of gyration, travel distance, entropy, and related indicators
-
-Co-location & Contact Networks
-Build proximity graphs from POI visits or spatial-temporal proximity
-
-Aggregation & Debiasing
-Differential privacy, k-anonymity, debiasing, and post-stratification
-
-Synthetic Data Generation
-EPR models and point process samplers for trajectory generation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/_build/html/modules.html b/docs/_build/html/modules.html
deleted file mode 100644
index f5253134..00000000
--- a/docs/_build/html/modules.html
+++ /dev/null
@@ -1,140 +0,0 @@
-
-
-
-
-
-
-
-
-
src — NOMAD 0.0.1 documentation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv
deleted file mode 100644
index 29e7fc8b..00000000
Binary files a/docs/_build/html/objects.inv and /dev/null differ
diff --git a/docs/_build/html/py-modindex.html b/docs/_build/html/py-modindex.html
deleted file mode 100644
index 298133a3..00000000
--- a/docs/_build/html/py-modindex.html
+++ /dev/null
@@ -1,225 +0,0 @@
-
-
-
-
-
-
-
-
Python Module Index — NOMAD 0.0.1 documentation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- NOMAD
-
-
-
-
-
-
-
- Python Module Index
-
-
-
-
-
-
-
-
-
-
Python Module Index
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/_build/html/search.html b/docs/_build/html/search.html
deleted file mode 100644
index ea508c0a..00000000
--- a/docs/_build/html/search.html
+++ /dev/null
@@ -1,145 +0,0 @@
-
-
-
-
-
-
-
-
Search — NOMAD 0.0.1 documentation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- NOMAD
-
-
-
-
-
-
-
-
-
-
-
- Please activate JavaScript to enable the search functionality.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/_build/html/searchindex.js b/docs/_build/html/searchindex.js
deleted file mode 100644
index 3e96b2f6..00000000
--- a/docs/_build/html/searchindex.js
+++ /dev/null
@@ -1 +0,0 @@
-Search.setIndex({"alltitles": {"API Reference": [[17, null]], "Aggregation & Debiasing": [[0, null]], "Basic Installation": [[22, "basic-installation"]], "Benchmarking Stop Detection Algorithms": [[26, null]], "Co-location & Contact Networks": [[19, null]], "Community & Support": [[23, "community-support"]], "DBSCAN": [[33, "dbscan"]], "Data Ingestion": [[20, null]], "Download Data by Bounding Box": [[30, "Download-Data-by-Bounding-Box"]], "Download Data by City Name": [[30, "Download-Data-by-City-Name"]], "Downloading places of interest (POI) Data from OSM": [[30, null]], "Examples": [[22, "examples"]], "Filtering & Completeness": [[21, null]], "Generating synthetic places of interest (POI) Data": [[31, null]], "Getting Started": [[22, null]], "Grid-Based Stop Detection": [[27, null]], "Grid-based": [[33, "grid-based"]], "HDBSCAN": [[33, "hdbscan"]], "HDBSCAN Stop Detection": [[28, null]], "Indices and tables": [[23, "indices-and-tables"]], "Installation": [[22, "installation"], [23, "installation"]], "Key Features": [[22, "key-features"]], "Lachesis": [[33, "lachesis"]], "Lachesis Stop Detection": [[29, null]], "License": [[22, "license"]], "Mobility Metrics": [[24, null]], "Module contents": [[18, "module-contents"]], "Modules": [[17, "modules"]], "Modules Overview": [[23, "modules-overview"]], "NOMAD: Network for Open Mobility Analysis and Data": [[23, null]], "Next Steps": [[22, "next-steps"]], "Overall Runtime Comparison": [[26, "Overall-Runtime-Comparison"]], "Overview of stop-detection methods": [[33, "overview-of-stop-detection-methods"]], "Parameters:": [[3, "parameters"]], "Performance Comparison": [[26, "Performance-Comparison"]], "Quick Links": [[23, "quick-links"]], "Quick Start": [[22, "quick-start"]], "Requirements": [[22, "requirements"]], "Returns:": [[3, "returns"]], "Runtime Scalability": [[26, "Runtime-Scalability"]], "Setup": [[26, "Setup"]], "Stop Detection": [[33, null]], "Submodules": [[18, "submodules"]], "Synthetic Data Generation": [[34, null]], "TADBSCAN Stop Detection": [[32, null]], "Tessellation": [[35, null]], "Visit Attribution": [[36, null]], "Visualization": [[26, "Visualization"]], "What is NOMAD?": [[22, "what-is-nomad"]], "With Spark Support": [[22, "with-spark-support"]], "agg": [[1, null]], "aggregation": [[2, null]], "base_module package": [[18, null]], "base_module.doc_test module": [[18, "base-module-doc-test-module"]], "base_module.setup module": [[18, "base-module-setup-module"]], "city_gen": [[3, null]], "constants": [[4, null]], "contact_estimation": [[5, null]], "displacement": [[6, null]], "filters": [[7, null]], "filters_spark": [[8, null]], "generation": [[9, null]], "home_attribution": [[10, null]], "io": [[11, null]], "map_utils": [[12, null]], "metrics": [[13, null]], "src": [[25, null]], "stop_detection": [[14, null]], "traj_gen": [[15, null]], "visit_attribution": [[16, null]]}, "docnames": ["aggregation", "api/nomad.agg", "api/nomad.aggregation", "api/nomad.city_gen", "api/nomad.constants", "api/nomad.contact_estimation", "api/nomad.displacement", "api/nomad.filters", "api/nomad.filters_spark", "api/nomad.generation", "api/nomad.home_attribution", "api/nomad.io", "api/nomad.map_utils", "api/nomad.metrics", "api/nomad.stop_detection", "api/nomad.traj_gen", "api/nomad.visit_attribution", "api_reference", "base_module", "colocation", "data_ingestion", "filtering", "getting_started", "index", "metrics", "modules", "source/benchmarking_of_stop_detection_algorithms", "source/grid_based_demo", "source/hdbscan_demo", "source/lachesis_demo", "source/poi_osm", "source/poi_synthetic", "source/tadbscan_demo", "stop_detection", "synthetic_data_generation", "tessellation", "visit_attribution"], "envversion": {"nbsphinx": 4, "sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.todo": 2, "sphinx.ext.viewcode": 1}, "filenames": ["aggregation.rst", "api\\nomad.agg.rst", "api\\nomad.aggregation.rst", "api\\nomad.city_gen.rst", "api\\nomad.constants.rst", "api\\nomad.contact_estimation.rst", "api\\nomad.displacement.rst", "api\\nomad.filters.rst", "api\\nomad.filters_spark.rst", "api\\nomad.generation.rst", "api\\nomad.home_attribution.rst", "api\\nomad.io.rst", "api\\nomad.map_utils.rst", "api\\nomad.metrics.rst", "api\\nomad.stop_detection.rst", "api\\nomad.traj_gen.rst", "api\\nomad.visit_attribution.rst", "api_reference.rst", "base_module.rst", "colocation.rst", "data_ingestion.rst", "filtering.rst", "getting_started.rst", "index.rst", "metrics.rst", "modules.rst", "source\\benchmarking_of_stop_detection_algorithms.ipynb", "source\\grid_based_demo.ipynb", "source\\hdbscan_demo.ipynb", "source\\lachesis_demo.ipynb", "source\\poi_osm.ipynb", "source\\poi_synthetic.ipynb", "source\\tadbscan_demo.ipynb", "stop_detection.rst", "synthetic_data_generation.rst", "tessellation.rst", "visit_attribution.rst"], "indexentries": {"__init__() (agent method)": [[15, "nomad.traj_gen.Agent.__init__", false]], "__init__() (city method)": [[3, "nomad.city_gen.City.__init__", false]], "__init__() (population method)": [[15, "nomad.traj_gen.Population.__init__", false]], "__init__() (randomcitygenerator method)": [[3, "nomad.city_gen.RandomCityGenerator.__init__", false]], "__init__() (street method)": [[3, "nomad.city_gen.Street.__init__", false]], "add_agent() (population method)": [[15, "nomad.traj_gen.Population.add_agent", false]], "add_building() (city method)": [[3, "id0", false], [3, "nomad.city_gen.City.add_building", false]], "add_buildings_from_gdf() (city method)": [[3, "nomad.city_gen.City.add_buildings_from_gdf", false]], "add_street() (city method)": [[3, "nomad.city_gen.City.add_street", false]], "agent (class in nomad.traj_gen)": [[15, "nomad.traj_gen.Agent", false]], "allowed_buildings() (in module nomad.traj_gen)": [[15, "nomad.traj_gen.allowed_buildings", false]], "blocks_to_mercator() (in module nomad.map_utils)": [[12, "nomad.map_utils.blocks_to_mercator", false]], "buildings (city attribute)": [[3, "nomad.city_gen.City.buildings", false]], "buildings_df (city property)": [[3, "nomad.city_gen.City.buildings_df", false]], "buildings_outline (city attribute)": [[3, "nomad.city_gen.City.buildings_outline", false]], "check_adjacent() (in module nomad.city_gen)": [[3, "nomad.city_gen.check_adjacent", false]], "city (class in nomad.city_gen)": [[3, "nomad.city_gen.City", false]], "city (population attribute)": [[15, "nomad.traj_gen.Population.city", false]], "city_boundary (city attribute)": [[3, "nomad.city_gen.City.city_boundary", false]], "completeness() (in module nomad.filters)": [[7, "nomad.filters.completeness", false]], "compute_candidate_homes() (in module nomad.home_attribution)": [[10, "nomad.home_attribution.compute_candidate_homes", false]], "compute_stop_detection_metrics() (in module nomad.contact_estimation)": [[5, "nomad.contact_estimation.compute_stop_detection_metrics", false]], "compute_visitation_errors() (in module nomad.contact_estimation)": [[5, "nomad.contact_estimation.compute_visitation_errors", false]], "condense_destinations() (in module nomad.traj_gen)": [[15, "nomad.traj_gen.condense_destinations", false]], "coordinates (street attribute)": [[3, "nomad.city_gen.Street.coordinates", false]], "coverage_matrix() (in module nomad.filters)": [[7, "nomad.filters.coverage_matrix", false]], "dimensions (city attribute)": [[3, "nomad.city_gen.City.dimensions", false]], "download_osm_buildings() (in module nomad.map_utils)": [[12, "nomad.map_utils.download_osm_buildings", false]], "download_osm_streets() (in module nomad.map_utils)": [[12, "nomad.map_utils.download_osm_streets", false]], "downsample() (in module nomad.filters)": [[7, "nomad.filters.downsample", false]], "dt (agent attribute)": [[15, "nomad.traj_gen.Agent.dt", false]], "dt (population attribute)": [[15, "nomad.traj_gen.Population.dt", false]], "fill_block() (randomcitygenerator method)": [[3, "nomad.city_gen.RandomCityGenerator.fill_block", false]], "filter_users() (in module nomad.filters_spark)": [[8, "nomad.filters_spark.filter_users", false]], "from_geodataframes() (city class method)": [[3, "nomad.city_gen.City.from_geodataframes", false]], "from_geopackage() (city class method)": [[3, "nomad.city_gen.City.from_geopackage", false]], "from_mercator() (city method)": [[3, "nomad.city_gen.City.from_mercator", false]], "generate_agents() (population method)": [[15, "nomad.traj_gen.Population.generate_agents", false]], "generate_city() (randomcitygenerator method)": [[3, "nomad.city_gen.RandomCityGenerator.generate_city", false]], "generate_streets() (randomcitygenerator method)": [[3, "nomad.city_gen.RandomCityGenerator.generate_streets", false]], "generate_trajectory() (agent method)": [[15, "nomad.traj_gen.Agent.generate_trajectory", false]], "geometry (street attribute)": [[3, "nomad.city_gen.Street.geometry", false]], "get_adjacent_street() (randomcitygenerator method)": [[3, "nomad.city_gen.RandomCityGenerator.get_adjacent_street", false]], "get_block() (city method)": [[3, "id1", false], [3, "nomad.city_gen.City.get_block", false]], "get_block_type() (randomcitygenerator method)": [[3, "nomad.city_gen.RandomCityGenerator.get_block_type", false]], "get_building() (city method)": [[3, "nomad.city_gen.City.get_building", false]], "get_building_coordinates() (city method)": [[3, "nomad.city_gen.City.get_building_coordinates", false]], "get_category_for_subtype() (in module nomad.map_utils)": [[12, "nomad.map_utils.get_category_for_subtype", false]], "get_category_summary() (in module nomad.map_utils)": [[12, "nomad.map_utils.get_category_summary", false]], "get_city_boundary_osm() (in module nomad.map_utils)": [[12, "nomad.map_utils.get_city_boundary_osm", false]], "get_prominent_streets() (in module nomad.map_utils)": [[12, "nomad.map_utils.get_prominent_streets", false]], "get_shortest_path() (city method)": [[3, "nomad.city_gen.City.get_shortest_path", false]], "get_street_graph() (city method)": [[3, "id2", false], [3, "nomad.city_gen.City.get_street_graph", false]], "get_subtype_summary() (in module nomad.map_utils)": [[12, "nomad.map_utils.get_subtype_summary", false]], "gravity (city attribute)": [[3, "nomad.city_gen.City.gravity", false]], "id (street attribute)": [[3, "nomad.city_gen.Street.id", false]], "id_to_door_cell() (city method)": [[3, "nomad.city_gen.City.id_to_door_cell", false]], "is_within() (in module nomad.filters)": [[7, "nomad.filters.is_within", false]], "load() (in module nomad.city_gen)": [[3, "nomad.city_gen.load", false]], "mercator_to_blocks() (in module nomad.map_utils)": [[12, "nomad.map_utils.mercator_to_blocks", false]], "module": [[1, "module-nomad.agg", false], [2, "module-nomad.aggregation", false], [3, "module-nomad.city_gen", false], [4, "module-nomad.constants", false], [5, "module-nomad.contact_estimation", false], [6, "module-nomad.displacement", false], [7, "module-nomad.filters", false], [8, "module-nomad.filters_spark", false], [9, "module-nomad.generation", false], [10, "module-nomad.home_attribution", false], [11, "module-nomad.io", false], [12, "module-nomad.map_utils", false], [13, "module-nomad.metrics", false], [14, "module-nomad.stop_detection", false], [15, "module-nomad.traj_gen", false], [16, "module-nomad.visit_attribution", false]], "nocturnal_stops() (in module nomad.home_attribution)": [[10, "nomad.home_attribution.nocturnal_stops", false]], "nomad.agg": [[1, "module-nomad.agg", false]], "nomad.aggregation": [[2, "module-nomad.aggregation", false]], "nomad.city_gen": [[3, "module-nomad.city_gen", false]], "nomad.constants": [[4, "module-nomad.constants", false]], "nomad.contact_estimation": [[5, "module-nomad.contact_estimation", false]], "nomad.displacement": [[6, "module-nomad.displacement", false]], "nomad.filters": [[7, "module-nomad.filters", false]], "nomad.filters_spark": [[8, "module-nomad.filters_spark", false]], "nomad.generation": [[9, "module-nomad.generation", false]], "nomad.home_attribution": [[10, "module-nomad.home_attribution", false]], "nomad.io": [[11, "module-nomad.io", false]], "nomad.map_utils": [[12, "module-nomad.map_utils", false]], "nomad.metrics": [[13, "module-nomad.metrics", false]], "nomad.stop_detection": [[14, "module-nomad.stop_detection", false]], "nomad.traj_gen": [[15, "module-nomad.traj_gen", false]], "nomad.visit_attribution": [[16, "module-nomad.visit_attribution", false]], "normalize_od() (in module nomad.displacement)": [[6, "nomad.displacement.normalize_od", false]], "normalized_moved() (in module nomad.displacement)": [[6, "nomad.displacement.normalized_moved", false]], "normalized_remained() (in module nomad.displacement)": [[6, "nomad.displacement.normalized_remained", false]], "overlapping_visits() (in module nomad.contact_estimation)": [[5, "nomad.contact_estimation.overlapping_visits", false]], "parse_agent_attr() (in module nomad.traj_gen)": [[15, "nomad.traj_gen.parse_agent_attr", false]], "place_buildings_in_blocks() (randomcitygenerator method)": [[3, "nomad.city_gen.RandomCityGenerator.place_buildings_in_blocks", false]], "plot_city() (city method)": [[3, "id4", false], [3, "nomad.city_gen.City.plot_city", false]], "plot_traj() (agent method)": [[15, "id0", false], [15, "nomad.traj_gen.Agent.plot_traj", false]], "population (class in nomad.traj_gen)": [[15, "nomad.traj_gen.Population", false]], "precision_recall_f1_from_minutes() (in module nomad.contact_estimation)": [[5, "nomad.contact_estimation.precision_recall_f1_from_minutes", false]], "q_filter() (in module nomad.filters)": [[7, "nomad.filters.q_filter", false]], "randomcitygenerator (class in nomad.city_gen)": [[3, "nomad.city_gen.RandomCityGenerator", false]], "remove_overlaps() (in module nomad.map_utils)": [[12, "nomad.map_utils.remove_overlaps", false]], "reproject_to_mercator() (population method)": [[15, "nomad.traj_gen.Population.reproject_to_mercator", false]], "reset_trajectory() (agent method)": [[15, "nomad.traj_gen.Agent.reset_trajectory", false]], "roster (population attribute)": [[15, "nomad.traj_gen.Population.roster", false]], "rotate() (in module nomad.map_utils)": [[12, "nomad.map_utils.rotate", false]], "rotate_streets_to_align() (in module nomad.map_utils)": [[12, "nomad.map_utils.rotate_streets_to_align", false]], "sample_hier_nhpp() (in module nomad.traj_gen)": [[15, "nomad.traj_gen.sample_hier_nhpp", false]], "sample_traj_hier_nhpp() (agent method)": [[15, "nomad.traj_gen.Agent.sample_traj_hier_nhpp", false]], "sample_trajectory() (agent method)": [[15, "nomad.traj_gen.Agent.sample_trajectory", false]], "save() (city method)": [[3, "id3", false], [3, "nomad.city_gen.City.save", false]], "save() (in module nomad.city_gen)": [[3, "nomad.city_gen.save", false]], "save_geopackage() (city method)": [[3, "nomad.city_gen.City.save_geopackage", false]], "save_pop() (population method)": [[15, "nomad.traj_gen.Population.save_pop", false]], "select_home() (in module nomad.home_attribution)": [[10, "nomad.home_attribution.select_home", false]], "set_osmnx_cache_mode() (in module nomad.map_utils)": [[12, "nomad.map_utils.set_osmnx_cache_mode", false]], "shortest_paths (city attribute)": [[3, "nomad.city_gen.City.shortest_paths", false]], "speeds (agent attribute)": [[15, "nomad.traj_gen.Agent.speeds", false]], "still_probs (agent attribute)": [[15, "nomad.traj_gen.Agent.still_probs", false]], "street (class in nomad.city_gen)": [[3, "nomad.city_gen.Street", false]], "street_adjacency_edges() (city method)": [[3, "nomad.city_gen.City.street_adjacency_edges", false]], "street_graph (city attribute)": [[3, "nomad.city_gen.City.street_graph", false]], "streets (city attribute)": [[3, "nomad.city_gen.City.streets", false]], "streets_df (city property)": [[3, "nomad.city_gen.City.streets_df", false]], "to_file() (city method)": [[3, "nomad.city_gen.City.to_file", false]], "to_geodataframes() (city method)": [[3, "nomad.city_gen.City.to_geodataframes", false]], "to_mercator() (city method)": [[3, "nomad.city_gen.City.to_mercator", false]], "to_projection() (in module nomad.filters)": [[7, "nomad.filters.to_projection", false]], "to_projection() (in module nomad.filters_spark)": [[8, "nomad.filters_spark.to_projection", false]], "to_tessellation() (in module nomad.filters)": [[7, "nomad.filters.to_tessellation", false]], "to_timestamp() (in module nomad.filters)": [[7, "nomad.filters.to_timestamp", false]], "to_yyyymmdd() (in module nomad.filters)": [[7, "nomad.filters.to_yyyymmdd", false]], "to_zoned_datetime() (in module nomad.filters)": [[7, "nomad.filters.to_zoned_datetime", false]], "within() (in module nomad.filters)": [[7, "nomad.filters.within", false]]}, "objects": {"nomad": [[1, 0, 0, "-", "agg"], [2, 0, 0, "-", "aggregation"], [3, 0, 0, "-", "city_gen"], [4, 0, 0, "-", "constants"], [5, 0, 0, "-", "contact_estimation"], [6, 0, 0, "-", "displacement"], [7, 0, 0, "-", "filters"], [8, 0, 0, "-", "filters_spark"], [9, 0, 0, "-", "generation"], [10, 0, 0, "-", "home_attribution"], [11, 0, 0, "-", "io"], [12, 0, 0, "-", "map_utils"], [13, 0, 0, "-", "metrics"], [14, 0, 0, "-", "stop_detection"], [15, 0, 0, "-", "traj_gen"], [16, 0, 0, "-", "visit_attribution"]], "nomad.city_gen": [[3, 1, 1, "", "City"], [3, 1, 1, "", "RandomCityGenerator"], [3, 1, 1, "", "Street"], [3, 5, 1, "", "check_adjacent"], [3, 5, 1, "", "load"], [3, 5, 1, "", "save"]], "nomad.city_gen.City": [[3, 2, 1, "", "__init__"], [3, 2, 1, "id0", "add_building"], [3, 2, 1, "", "add_buildings_from_gdf"], [3, 2, 1, "", "add_street"], [3, 3, 1, "", "buildings"], [3, 4, 1, "", "buildings_df"], [3, 3, 1, "", "buildings_outline"], [3, 3, 1, "", "city_boundary"], [3, 3, 1, "", "dimensions"], [3, 2, 1, "", "from_geodataframes"], [3, 2, 1, "", "from_geopackage"], [3, 2, 1, "", "from_mercator"], [3, 2, 1, "id1", "get_block"], [3, 2, 1, "", "get_building"], [3, 2, 1, "", "get_building_coordinates"], [3, 2, 1, "", "get_shortest_path"], [3, 2, 1, "id2", "get_street_graph"], [3, 3, 1, "", "gravity"], [3, 2, 1, "", "id_to_door_cell"], [3, 2, 1, "id4", "plot_city"], [3, 2, 1, "id3", "save"], [3, 2, 1, "", "save_geopackage"], [3, 3, 1, "", "shortest_paths"], [3, 2, 1, "", "street_adjacency_edges"], [3, 3, 1, "", "street_graph"], [3, 3, 1, "", "streets"], [3, 4, 1, "", "streets_df"], [3, 2, 1, "", "to_file"], [3, 2, 1, "", "to_geodataframes"], [3, 2, 1, "", "to_mercator"]], "nomad.city_gen.RandomCityGenerator": [[3, 2, 1, "", "__init__"], [3, 2, 1, "", "fill_block"], [3, 2, 1, "", "generate_city"], [3, 2, 1, "", "generate_streets"], [3, 2, 1, "", "get_adjacent_street"], [3, 2, 1, "", "get_block_type"], [3, 2, 1, "", "place_buildings_in_blocks"]], "nomad.city_gen.Street": [[3, 2, 1, "", "__init__"], [3, 3, 1, "", "coordinates"], [3, 3, 1, "", "geometry"], [3, 3, 1, "", "id"]], "nomad.contact_estimation": [[5, 5, 1, "", "compute_stop_detection_metrics"], [5, 5, 1, "", "compute_visitation_errors"], [5, 5, 1, "", "overlapping_visits"], [5, 5, 1, "", "precision_recall_f1_from_minutes"]], "nomad.displacement": [[6, 5, 1, "", "normalize_od"], [6, 5, 1, "", "normalized_moved"], [6, 5, 1, "", "normalized_remained"]], "nomad.filters": [[7, 5, 1, "", "completeness"], [7, 5, 1, "", "coverage_matrix"], [7, 5, 1, "", "downsample"], [7, 5, 1, "", "is_within"], [7, 5, 1, "", "q_filter"], [7, 5, 1, "", "to_projection"], [7, 5, 1, "", "to_tessellation"], [7, 5, 1, "", "to_timestamp"], [7, 5, 1, "", "to_yyyymmdd"], [7, 5, 1, "", "to_zoned_datetime"], [7, 5, 1, "", "within"]], "nomad.filters_spark": [[8, 5, 1, "", "filter_users"], [8, 5, 1, "", "to_projection"]], "nomad.home_attribution": [[10, 5, 1, "", "compute_candidate_homes"], [10, 5, 1, "", "nocturnal_stops"], [10, 5, 1, "", "select_home"]], "nomad.map_utils": [[12, 5, 1, "", "blocks_to_mercator"], [12, 5, 1, "", "download_osm_buildings"], [12, 5, 1, "", "download_osm_streets"], [12, 5, 1, "", "get_category_for_subtype"], [12, 5, 1, "", "get_category_summary"], [12, 5, 1, "", "get_city_boundary_osm"], [12, 5, 1, "", "get_prominent_streets"], [12, 5, 1, "", "get_subtype_summary"], [12, 5, 1, "", "mercator_to_blocks"], [12, 5, 1, "", "remove_overlaps"], [12, 5, 1, "", "rotate"], [12, 5, 1, "", "rotate_streets_to_align"], [12, 5, 1, "", "set_osmnx_cache_mode"]], "nomad.traj_gen": [[15, 1, 1, "", "Agent"], [15, 1, 1, "", "Population"], [15, 5, 1, "", "allowed_buildings"], [15, 5, 1, "", "condense_destinations"], [15, 5, 1, "", "parse_agent_attr"], [15, 5, 1, "", "sample_hier_nhpp"]], "nomad.traj_gen.Agent": [[15, 2, 1, "", "__init__"], [15, 3, 1, "", "dt"], [15, 2, 1, "", "generate_trajectory"], [15, 2, 1, "id0", "plot_traj"], [15, 2, 1, "", "reset_trajectory"], [15, 2, 1, "", "sample_traj_hier_nhpp"], [15, 2, 1, "", "sample_trajectory"], [15, 3, 1, "", "speeds"], [15, 3, 1, "", "still_probs"]], "nomad.traj_gen.Population": [[15, 2, 1, "", "__init__"], [15, 2, 1, "", "add_agent"], [15, 3, 1, "", "city"], [15, 3, 1, "", "dt"], [15, 2, 1, "", "generate_agents"], [15, 2, 1, "", "reproject_to_mercator"], [15, 3, 1, "", "roster"], [15, 2, 1, "", "save_pop"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "attribute", "Python attribute"], "4": ["py", "property", "Python property"], "5": ["py", "function", "Python function"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:attribute", "4": "py:property", "5": "py:function"}, "terms": {"": [3, 6, 7, 8, 12, 15, 22, 26, 31], "0": [3, 7, 12, 15, 26, 30, 31], "01": [26, 27, 28, 29, 32], "0204081632653061": 15, "03": 32, "05": [], "060303": 26, "070307": 30, "08": 30, "08098": 30, "0e0e0": [], "1": [3, 7, 8, 12, 15, 22, 26, 27, 28, 29, 30, 31, 32], "10": [12, 26, 27, 28, 29, 30, 31, 32], "100": [6, 22], "1004": 30, "101": 30, "104": [], "106": 31, "11": 31, "11749631": 30, "11752856": 30, "11753099": 30, "11758546": 30, "12": [26, 31], "120": [], "13": 31, "14": [26, 30, 31], "14652246706544": 30, "15": [3, 12, 15, 26, 27, 28, 31, 32], "154": 30, "155": 30, "16": 31, "16617": 30, "16631": 30, "17": 31, "1700": 30, "17019": 30, "1704162819": 26, "17178": 30, "172": 30, "18": [30, 31], "180": 30, "18000": 7, "19": [10, 31], "191": 30, "19103": 30, "19104": 30, "19129": 30, "19130": 30, "19375": 30, "19378": 30, "19743": 30, "19745": 30, "19747721789525": 30, "2": [3, 12, 15, 22, 26, 28, 30, 31], "20": [26, 29, 31], "200": 12, "2024": [27, 28, 29, 32], "2025": [22, 23, 27], "2035": 30, "207118": 26, "21": [27, 30, 31], "22": 31, "224": 31, "23": [], "23172": 30, "240": [26, 27, 28, 32], "246": 30, "25": [], "255": [], "2c353c": [], "3": [3, 7, 22, 26, 28, 30, 31, 32], "30": 28, "300": [], "314632": 26, "31779": 30, "31782": 30, "32": 30, "323719": 30, "33003": 30, "3330": 30, "33482": 30, "34": [], "3600": 26, "36042": 30, "37962": 30, "3826530612244898": 15, "3857": [8, 12, 27, 28, 31, 32], "39": 30, "3f": [26, 31], "4": [3, 22, 26, 30, 31, 32], "4075": 30, "4096": [], "42": 3, "4265669": 12, "4265684": 12, "4265699": [3, 12], "4326": [12, 26, 32], "4392976": [3, 12], "4392991": 12, "450": [], "47604": 30, "47605": 30, "47648": 30, "47653": 30, "4766": 30, "48": 26, "5": [3, 7, 15, 26, 27, 28, 29, 30, 31, 32], "50": [], "504672": 26, "51444": 30, "515": 15, "51511": 30, "525774": 30, "5574": 30, "569": 30, "58": 30, "59249": 30, "6": [10, 26, 30, 31], "60": [26, 29], "64045": 30, "64079": 30, "64503": 30, "648": 30, "64825": 30, "656": 31, "68": 15, "6h": 26, "7": [30, 31], "72": 30, "720": [28, 32], "72495": 30, "72523": 30, "75": [15, 30], "7597": 30, "7747": 30, "7932": 30, "8": [15, 31], "80": [], "8192": [], "83": 30, "8601": [], "8928571428571429": 15, "9": [15, 22, 31], "931392279878246": 30, "947": 30, "94717": 30, "949": 30, "94942": 30, "95201": 30, "95204": 30, "953576": 30, "95649": 30, "95652": 30, "962": 30, "96231": 30, "96336810441389": 30, "99": [], "A": [3, 7, 8, 15, 33], "AND": [], "At": [], "But": [], "For": [7, 15, 22], "If": [3, 6, 7, 8, 12, 15], "In": [], "It": [8, 10, 33], "Its": [], "NOT": [], "No": 5, "On": [], "One": 7, "The": [3, 5, 7, 12, 15, 22, 27, 28, 29, 31, 32], "There": [], "These": 15, "To": [0, 7, 19, 20, 21, 24, 33, 34, 35, 36], "__init__": [3, 15], "_add_mani": 31, "_base_cdf": [], "about": [3, 15], "abov": [], "absent": 7, "absolut": [], "abstract": 12, "accept": [7, 31], "access": 30, "accuraci": 15, "across": 31, "act": [], "actual": [7, 15], "ad": [3, 15], "adapt": [32, 33], "add": [3, 6, 15, 31], "add_ag": 15, "add_build": [3, 31], "add_buildings_from_gdf": 3, "add_point": [], "add_street": 3, "addit": [7, 8, 15], "addr": 30, "address": [3, 15, 31], "adjac": 3, "adjust_zoom": [], "admiring_brattain": [27, 28, 29], "affin": 12, "after": [7, 15], "against": [], "agent": [3, 15], "agent_hom": 15, "agent_workplac": 15, "agg_freq": 7, "aggreg": [7, 10, 23], "aid": [22, 23], "algo": 26, "algorithm": [5, 22, 23, 27, 28, 29, 32, 33], "alia": [], "alias": 7, "align": [3, 7, 12], "all": [3, 7, 10, 12, 15, 22, 23], "allow": 15, "allowai": 30, "allowed_build": 15, "along": [], "alpha": [3, 15, 26, 30], "alreadi": [5, 10, 15], "also": [3, 12, 15, 32], "alter": 3, "altern": 27, "although": [], "alwai": [], "amount": 29, "an": [3, 5, 6, 12, 15, 22, 23, 29, 32], "analysi": 22, "ani": [3, 7, 12], "anonym": 23, "anoth": [8, 12], "any_coord": 3, "apart": 30, "api": [22, 23, 30], "appear": 7, "append": [15, 26], "appli": [7, 8, 12], "applic": 3, "approach": [], "appropri": 5, "approxim": [], "ar": [3, 6, 7, 8, 10, 12, 15, 23, 27, 32], "area": 8, "arg": [], "argument": [7, 15], "around": 12, "arrai": 15, "arriv": 15, "artefact": [], "assess": [22, 23], "assign": [3, 7, 26, 28, 32], "assum": 10, "attempt": 8, "attr": 15, "attribut": [15, 22, 23], "augment": [], "author": 12, "auto": [], "autodetect": [], "automat": [15, 17, 33], "avail": 3, "avenu": 30, "avoid": [], "awai": 29, "awar": 33, "ax": [3, 15, 26, 27, 28, 29, 30, 31, 32], "ax1": 26, "ax2": 26, "ax_barcod": [26, 27, 28, 29, 32], "ax_map": 26, "axi": [3, 15, 26], "b": [], "back": [3, 12], "backend": [], "background": [], "background_gdf": [], "bailei": 30, "bar": 26, "barcod": [], "barh": 26, "base": [3, 15, 22, 23, 26, 28, 29, 32], "base_geom_background": [], "base_geom_color": [], "base_geometri": 26, "base_modul": 25, "basic": [], "bbox": [12, 30], "bbox_or_c": 12, "bear": 12, "becaus": [], "been": [], "befor": [], "begin": [], "behav": [], "being": [], "below": 33, "beta_dur": 15, "beta_dur_rang": [], "beta_p": 15, "beta_ping_rang": [], "beta_start": 15, "beta_start_rang": [], "between": [3, 10, 29, 32, 33], "beyond": 10, "binari": [], "black": [15, 26, 30], "blksize": [], "block": [3, 12, 15, 30, 31], "block_i": 3, "block_side_length": [3, 15], "block_siz": 12, "block_typ": 3, "block_x": 3, "blockingioerror": [], "blocks_gdf": 3, "blocks_to_merc": 12, "blue": [28, 29, 30], "bool": [3, 7, 15], "boolean": 7, "border": [], "both": [8, 26], "bound": [3, 7, 8, 12], "boundari": [3, 12], "boundary_multipolygon": 12, "box": [3, 12, 27, 28, 29, 31, 32], "bridg": 30, "broken": [], "btype": 31, "bucket": 7, "budget": 6, "buffer": [27, 28, 32], "buffer_s": [], "bufferediobas": [], "bufferedrandom": [], "bufferedread": [], "bufferedrwpair": [], "bufferedwrit": [], "bug": 23, "build": [3, 12, 15, 22, 23, 30, 31], "building_id": [3, 5, 15], "building_typ": [3, 31], "buildings_df": 3, "buildings_gdf": [3, 15, 31], "buildings_outlin": 3, "buildings_path": 3, "built": 31, "builtin": [], "burst": 15, "by_chunk": 12, "byte": [], "bytearrai": [], "bytesio": [], "c": [], "cach": 12, "cache_mod": 12, "cache_traj": 15, "calcul": [3, 10], "calendar": [], "call": [27, 31], "callabl": 15, "caller": [5, 10], "can": [3, 15, 29], "candid": [], "candidate_hom": 10, "candidate_workplac": [], "cannot": 12, "canon": 15, "carolinechen": [], "carri": [], "carrol": 30, "case": [7, 33], "cast": [], "categor": 12, "categori": [12, 30], "cdf": [], "cdf_function": [], "cell": [], "center": 26, "center_coordin": 12, "centroid": 12, "certain": [], "chain": [], "chang": [22, 23], "charact": [], "characterist": 33, "check_adjac": 3, "choos": [], "chosen": [], "chronolog": [], "chunk": [], "chunk_mil": 12, "circl": [], "citi": [3, 12, 15, 26, 27, 28, 31, 32], "city_boundari": 3, "city_gen": 31, "city_nam": 30, "city_properti": 3, "class": [3, 15], "classmethod": 3, "clean": 12, "client": [], "clip": [10, 12, 30], "clip_spatial_outli": 26, "clip_stays_d": [], "clip_stops_datetim": [], "clip_to_gdf": 12, "close": 31, "closefd": [], "closest": 3, "cluster": [22, 23, 27, 28, 32, 33], "cluster_hierarchi": [], "cluster_id": [], "cluster_label": [], "cluster_st": [], "cluster_stability_df": [], "cmap": 26, "co": [5, 23], "code": 8, "codec": [], "coercion": 10, "col_nam": 15, "collect": [], "color": [3, 15, 26, 30], "colormap": [], "column": [3, 5, 6, 7, 8, 10, 12, 15, 27], "column_stack": [7, 32], "colunmn": [], "com": [22, 23], "combin": [3, 5, 27], "commerci": [27, 30], "common": [7, 27], "compar": 26, "comparison": 33, "complet": [3, 7, 22, 23, 26], "complete_output": [26, 27, 28, 29, 32], "comput": [5, 6, 7, 8, 22, 31, 33], "compute_candidate_hom": 10, "compute_candidate_workplac": [], "compute_cluster_st": [], "compute_radius_of_gyr": 22, "compute_stop_detection_metr": 5, "compute_visitation_error": 5, "concaten": [], "concentr": [], "conceptu": [], "condense_destin": 15, "confident_aryabhata": 32, "conflict": [], "connect": 3, "consecut": [7, 15, 29, 32], "consid": [12, 29, 32], "construct": [3, 28], "constructor": [], "contact": 23, "contain": [3, 7, 8, 12, 15, 17, 22], "content": 25, "contigu": 29, "contribut": [6, 7, 23], "control": [15, 22], "convent": 7, "convers": 7, "convert": [3, 6, 7, 12], "cooki": [], "coord": 3, "coord_i": 3, "coord_x": 3, "coordin": [3, 7, 8, 12, 15, 27, 32], "core": 22, "core_dist": [], "correctli": [], "correspond": [7, 15], "count": [6, 12], "count_night": [], "counterclockwis": 12, "counterpart": [], "cove": 30, "cover": [12, 15, 22, 23], "coverag": 23, "coverage_matrix": 7, "cr": [7, 8, 12], "crash": [], "creat": 3, "creation": [], "criterion": [], "crs_to": [7, 32], "csv": [22, 23, 26], "cumul": [], "current": [], "current_end": 26, "current_idx": [], "custom": 23, "d": [7, 15], "d_graph": [], "dai": [7, 8], "daili": 7, "data": [3, 6, 7, 12, 22, 26, 27, 28, 29, 32, 33], "data_cr": [7, 26, 27, 32], "databas": [], "datafram": [3, 5, 6, 7, 8, 12, 15, 23, 26], "dataset": [22, 23, 26, 27], "date": [7, 27, 28, 29, 32], "date_rang": 26, "datetim": [7, 8, 10, 15, 26], "datetime64": [7, 27], "dawn_hour": 10, "dawn_tim": [], "day_part": [], "daytim": [], "dbscan": [22, 23, 26, 32], "dd": [], "deal": [], "debias": 23, "declar": 31, "decod": [], "dedic": [], "dedupl": 15, "def": 31, "default": [3, 7, 8, 12, 15, 27], "default_buffer_s": [], "default_schema": [], "default_spe": 15, "default_still_prob": 15, "defer": [], "defin": [7, 8, 32], "degre": 12, "delawar": 30, "delimit": [], "delta_roam": [26, 29], "demonstr": 22, "dendogram_scal": [], "densiti": [32, 33], "depend": [22, 29], "deprec": [], "deriv": [15, 22], "descend": [], "describ": [], "descript": 23, "descriptor": [], "design": 22, "desktop": [], "dest_col": 6, "destin": [6, 15], "destination_diari": 15, "detach": [], "detail": 3, "detect": [5, 22, 23], "detect_stop": 22, "determin": [3, 29, 32], "determinist": 33, "dev_i": [27, 28, 29, 32], "dev_x": [27, 28, 29, 32], "develop": 12, "devic": [], "df": [7, 12, 22], "diamet": 26, "diari": 15, "diaries_path": 15, "dict": [3, 5, 7, 8, 12, 15, 31], "dictionari": [3, 5, 7, 8, 15], "diff_privacy_ep": 6, "differ": [22, 27, 28, 32], "differenti": 23, "dimens": [3, 31, 32], "directli": [7, 22], "directori": [], "disabl": [], "disconnect": [], "discret": 15, "disk": 3, "dist_thresh": [26, 28, 32], "distanc": [3, 23, 27, 29, 32], "distance_threshold": 33, "distinct": 8, "distinguish": 33, "distribut": [3, 8, 15, 22], "divid": 12, "do": [3, 10], "doc_test": 25, "docstr": 17, "document": 17, "doe": [], "doesn": [], "don": [], "door": [3, 15, 31], "door_cell_i": 3, "door_cell_x": 3, "door_coord": 3, "door_point": 3, "down": 7, "download": 12, "download_osm_build": [12, 30], "download_osm_street": [12, 30], "downsampl": 7, "dp": 6, "draw": [], "drawn": 15, "drive": 30, "driver": [3, 30], "drop": 10, "dt": 15, "dt_max": [26, 29], "dtype": 7, "dual": 22, "dummi": [], "duplic": 7, "dur_min": [26, 28, 29, 32], "durat": [5, 10, 15, 29], "duration_at_night_fast": [], "dusk_hour": 10, "dusk_tim": [], "dx": 3, "dy": 3, "dynam": 3, "e": [3, 7, 8, 15, 31], "each": [3, 6, 7, 10, 15, 26, 31], "east": [3, 12], "edg": 3, "edge_alpha": [], "edge_cmap": [], "edge_color": [], "edge_onli": [], "edgecolor": 30, "edges_df": 3, "edges_path": 3, "edges_sorted_df": [], "edu": [22, 23], "educ": 30, "effect": [], "effici": 12, "egg": 22, "either": [7, 31], "elaps": 31, "element": 15, "els": 31, "empti": 15, "enabl": 23, "encod": 7, "end": [3, 7, 8, 15, 22, 23, 26], "end_": [], "end_col": [], "end_coord": 3, "end_datetim": 10, "end_tim": [8, 15], "end_timestamp": 26, "enforc": [], "engin": [], "ensur": 3, "entir": [22, 23], "entri": [7, 15, 31], "entropi": 23, "eof": [], "ep": [22, 33], "epoch": 7, "epr": [15, 23], "epr_time_r": 15, "epsg": [8, 12, 26, 27, 28, 31, 32], "epsilon": [], "equal": 30, "equival": [22, 23], "error": [12, 15], "estim": [5, 12], "etc": 5, "euclidean": 27, "evalu": 26, "even": [], "event": 15, "everi": 7, "exact": 6, "exactli": [], "exampl": [7, 12, 26], "except": [], "exclude_categori": 12, "exclus": [], "execut": 26, "exist": [3, 12, 15, 22, 23], "expect": [7, 8, 15], "explicit": 7, "explod": [12, 30], "explode_stop": [], "explor": 15, "exponenti": 15, "express": 7, "extend": [], "extra": [], "extract": 29, "f": [15, 26, 30, 31], "f1": 5, "face": [], "failur": [], "fall": [], "fallback": 3, "fals": [3, 5, 7, 12, 15, 26, 27, 28, 29, 30, 31, 32], "false_east": 12, "false_north": 12, "fast": 33, "faster": [], "favor": [], "featur": 12, "fetch": 12, "field": 15, "fig": [26, 27, 28, 29, 30, 31, 32], "figsiz": [26, 27, 28, 29, 30, 31, 32], "figur": 3, "file": [3, 15], "fileexistserror": [], "fileio": [], "filenam": 3, "fileno": [], "filepath": [], "filepath_root": [27, 28, 29, 32], "filesystem": 15, "fill": [3, 5], "fill_block": 3, "fill_cmap": [], "fill_timestamp_gap": [], "filter": [8, 12, 22, 23, 26, 27, 28, 29, 32], "filter_us": 8, "final": [], "find": [3, 15, 33], "first": [7, 26, 29], "first_tim": [], "fix": [], "fixed_format": [], "flag": 7, "flexibl": 12, "float": [3, 6, 7, 12, 15], "flow": 6, "flush": [], "fmt": 15, "focus": 22, "folder": 22, "follow": [15, 29], "form": [23, 32], "format": [3, 22, 26, 27, 28, 29, 32], "found": [3, 7], "four": 26, "fp": [], "frac_record": [], "frac_us": [], "fraction": [5, 7], "freq": [7, 26], "frequenc": [7, 23, 33], "frequent": [], "friendli": 7, "from": [3, 5, 7, 8, 12, 15, 17, 22, 23, 26, 27, 28, 29, 31, 32], "from_df": [], "from_fil": [], "from_geodatafram": 3, "from_geopackag": 3, "from_merc": 3, "full": [15, 26], "full_path": 15, "full_traj": 15, "fulli": [3, 12], "func": 26, "function": [8, 12, 15, 22, 23], "fund": [22, 23], "fundament": 33, "further": [], "futur": [], "g": [3, 7, 8, 15], "gap": [5, 29], "garden": [3, 30], "garden_c": [12, 26, 27, 28, 30, 32], "garden_city_categori": 30, "gc_data": 26, "gc_data_long": [27, 28, 29, 32], "gc_identifi": [27, 28, 29, 32], "gdf": 12, "gdf_row": 3, "gen_params_rang": [], "gen_params_target_q": [], "gener": [3, 15, 17, 22, 23, 33], "generate_ag": 15, "generate_c": 3, "generate_dest_diari": 15, "generate_street": 3, "generate_trajectori": 15, "geodatafram": [3, 7, 12], "geograph": [27, 32], "geographi": [6, 23], "geohash": [7, 27], "geojson": [3, 22, 23, 26, 27, 28, 30, 32], "geolife_plu": 12, "geom": [3, 31], "geom1": 3, "geom2": 3, "geometri": [3, 8, 12, 27, 28, 29, 30, 31, 32], "geopackag": [3, 31], "geopanda": [3, 22, 26, 27, 28, 30, 32], "geospati": 12, "get": [3, 31], "get_adjacent_street": 3, "get_block": 3, "get_block_typ": 3, "get_build": 3, "get_building_coordin": 3, "get_category_for_subtyp": 12, "get_category_summari": 12, "get_city_boundary_osm": 12, "get_height": 26, "get_i": 26, "get_prominent_street": 12, "get_shortest_path": 3, "get_street_graph": [3, 31], "get_subtype_summari": 12, "get_table_head": [], "getbuff": [], "getpreferredencod": [], "getvalu": [], "git": [22, 23], "github": [22, 23], "give": [], "given": [3, 6, 7, 12, 15, 27], "global": 12, "goal": [22, 23], "good": 33, "govern": 15, "gp": [22, 23, 33], "gpd": [3, 26, 27, 28, 30, 32], "gpkg": [3, 31], "gpkg_path": 3, "granular": 15, "graph": [3, 23, 31], "graviti": 3, "greater": 29, "green": [27, 30], "grei": 30, "grid": [3, 12, 23, 26], "grid_bas": [26, 27], "grid_based_label": [], "grid_based_per_us": [], "grid_resolut": 33, "gridspec_kw": 26, "ground": [5, 15], "group": [], "grouped_data": [], "gt": 30, "guidelin": 23, "gyrat": 23, "h": [3, 7], "h3": [7, 23, 26, 27], "h3_cell": [26, 27], "h3_cell_to_polygon": [], "h3_resolut": [], "ha": [3, 12, 15, 27, 28, 29, 31, 32], "handl": [7, 12, 23], "hariharan": 29, "hash": [], "hashabl": [], "have": [8, 15], "haversin": [27, 32], "hdbscan": [23, 26], "hdbscan_label": 28, "head": 30, "heatmap": [3, 15], "heatmap_ag": 3, "height": 3, "height_ratio": 26, "help": [], "helper": [10, 31], "here": [5, 22], "heterogen": 7, "heurist": [12, 23], "hexagon": [], "hierarch": [15, 33], "hierarchi": 28, "hierarchy_df": [], "higher": 22, "highlight": [], "highwai": [12, 30], "histori": 29, "hit": [], "hold": [], "hollow": [], "home": [3, 10, 15, 22, 23, 31], "home_d": [], "home_ratio": 3, "homes_path": 15, "homogen": 15, "horizont": 15, "hospit": 30, "hour": [7, 26], "hourli": [], "housenumb": 30, "how": [22, 26], "howev": [], "http": [22, 23], "human": 33, "i": [3, 5, 6, 7, 8, 10, 12, 15, 23, 27, 29, 32, 33], "ic2s2": 27, "id": [3, 7, 15], "id_col": [], "id_to_door_cel": 3, "ident": 12, "identifi": [3, 5, 7, 15, 29, 33], "ignor": 7, "immedi": [], "immin": [], "immut": [], "implement": [0, 19, 20, 21, 22, 23, 24, 27, 29, 32, 33, 34, 35, 36], "impli": [], "implicit": [], "import": [12, 22, 26, 27, 28, 29, 30, 31, 32], "includ": [7, 12, 15], "include_schema": [], "include_weekend": [], "incomplet": [], "incorpor": 32, "increas": 26, "increment": 26, "index": [7, 12, 15, 23, 26, 27], "indic": [7, 15], "individu": [3, 5, 7], "inf": [], "infer": [7, 10, 15, 22, 23], "infer_building_typ": [12, 30], "inform": [3, 15, 22], "infrastructur": [22, 23], "ingest": [22, 23], "inherit": [], "initi": [3, 15, 31], "initial_byt": [], "initial_valu": [], "inlin": [27, 28, 29, 32], "inplac": 31, "input": [7, 12, 27], "input_cr": 8, "insid": [7, 8], "insipir": 29, "instead": [3, 7], "int": [3, 6, 7, 8, 15], "int0": 15, "int64": 7, "integ": [7, 27], "intend": [], "intent": [], "inter": 15, "interact": [], "interchang": [], "interfac": [], "intern": [], "interpret": [], "intersect": 10, "interv": 7, "invalid": [3, 7], "invalid_stop": [], "invers": 12, "invok": [], "io": [22, 26, 27, 28, 29, 32], "iobas": [], "is_within": 7, "isatti": [], "isn": [], "iso": [], "issu": 23, "item": 30, "iter": [], "its": 15, "jersei": 30, "join": 15, "join_styl": [27, 28, 32], "junction": 30, "jupyt": 22, "k": [12, 23], "keep": [7, 12, 15], "keep_col_nam": [26, 29], "kei": [3, 7, 12, 15], "kept": [], "keyerror": 7, "keyword": [10, 15], "kind": 3, "known": [], "kwarg": [5, 7, 8, 10, 15, 31], "lab": [22, 23], "label": 26, "label_history_df": [], "lachesi": [23, 26], "lachesis_label": [], "lachesis_per_us": [], "lack": 3, "lambda": 26, "lane": 30, "laplac": 6, "larg": [22, 23], "larger": 26, "largest": [], "last": 7, "last_dat": [], "last_p": 15, "last_tim": [], "lat": [7, 27, 32], "latent": 15, "later": [], "latitud": [8, 26, 32], "layer": 3, "layout": 15, "lead": [], "learn": 22, "least": [7, 8, 27, 29], "leav": [], "left": 5, "legal": [], "legend": 26, "len": [26, 30, 31], "length": [3, 12, 15, 30], "less": [], "librari": [12, 22, 23], "licens": 23, "like": [8, 22, 23, 27], "limit": [], "line": [], "line_buff": [], "linesep": [], "linestr": 30, "linewidth": [26, 30], "list": [3, 6, 15, 31], "listedcolormap": [], "live": [], "load": [3, 22, 27, 28, 29, 32], "loader": [22, 26, 27, 28, 29, 32], "local": [7, 8, 15, 23], "local_t": 15, "localize_from_offset": [], "locat": [3, 5, 10, 15, 22, 23, 27, 29, 33], "location_id": [26, 27], "logic": 7, "lon": [7, 27, 32], "long": [], "longitud": [8, 26, 32], "loop": [], "loss": [], "low": 33, "lower": 7, "lower_quantil": [], "lt": 30, "m": [7, 15], "mai": 7, "main": [], "major": [], "make": [], "manag": 3, "mani": 31, "manual": 3, "manual_street": 3, "map": [3, 5, 7, 8, 15, 23, 30], "map_util": 30, "mark": [], "marker": 26, "mask": [3, 7], "massiv": [22, 23], "match": 7, "match_loc": 5, "matplotlib": [3, 15, 26, 27, 28, 29, 30, 31, 32], "matrix": 7, "matter": [], "max": 26, "max_dist": [], "max_gap": [], "maxim": 28, "maximum": [29, 32], "maxspe": 30, "mean": 15, "meaning": 33, "measur": [7, 26], "medic": 30, "medoid": [], "meet": [], "memori": 3, "mercat": [3, 12, 15], "mercator_to_block": 12, "mere": 10, "merg": 5, "merged_fract": 5, "messag": 15, "meter": [3, 12, 27, 29, 32], "method": [3, 15, 28, 32], "metric": [5, 22, 23], "metrics_df": 22, "might": [], "min": [7, 26], "min_active_dai": 8, "min_cluster_s": [26, 28, 32, 33], "min_dai": 10, "min_dur": 33, "min_dwel": [], "min_pings_per_dai": 8, "min_pt": [26, 28, 32], "min_sampl": [22, 33], "min_week": 10, "minimum": [29, 32], "minut": [5, 7, 15, 29, 32], "mismatch": 7, "miss": [5, 8], "missed_fract": 5, "mit": [22, 23], "mitr": [27, 28, 32], "mix": [], "mixed_timezone_behavior": 15, "mm": [], "mobil": [22, 33], "mobilkit": [22, 23], "mode": 12, "model": [15, 23, 29], "modifi": 15, "modul": [12, 25], "moment": [], "monthli": 7, "more": 22, "most": [7, 12], "move": [3, 6, 29], "movement": 33, "mst": [], "much": [], "multi": [], "multilinestr": 12, "multipl": [3, 7], "multipolygon": [3, 7, 12], "must": [3, 5, 7, 15, 29], "n": [15, 27, 31], "n_ping": 26, "n_row": [], "na": [7, 30], "naiv": 15, "naive_datetime_from_unix_and_offset": [], "naive_dt": [], "name": [3, 5, 7, 8, 10, 12, 15, 26, 27, 33], "name_count": 15, "nan": 30, "nearest": [], "necessari": 10, "need": 8, "neg": 6, "neighbor": [3, 32], "neither": [], "network": [12, 22], "networkx": 22, "never": [], "new": [3, 8, 15, 30], "newlin": [], "next": [], "night": 10, "night_stop": [], "nightli": 10, "nj": 30, "nocturn": 10, "nocturnal_stop": 10, "node": 3, "nois": 6, "nomad": [12, 17, 26, 27, 28, 29, 30, 31, 32, 33], "non": [6, 15, 28], "none": [3, 5, 6, 7, 8, 10, 12, 15], "nor": [], "normalis": 6, "normalize_od": 6, "normalized_mov": 6, "normalized_remain": 6, "north": [3, 12], "note": [], "notebook": [22, 23, 26], "noth": [], "notic": 32, "np": [7, 27, 28, 32], "nsampl": 30, "nsf": [22, 23], "num_night": [], "num_week": [], "num_work_dai": [], "number": [7, 15, 26, 32], "numpi": [3, 15, 22, 27, 28, 32], "o": 26, "object": [3, 7, 15, 29], "observ": 7, "obtain": [], "occupi": 3, "occurr": [], "od": 6, "od_df": [], "off": 12, "offend": [], "offset": [7, 12], "offset_col": 7, "often": [], "oi": 3, "omit": 7, "onc": [], "one": [7, 8, 12, 29], "ones": [], "onewai": 30, "onli": [3, 7, 15, 27, 30], "opac": [], "open": [22, 30], "open_cod": [], "openstreetmap": 12, "oper": 3, "optim": [], "option": [3, 5, 6, 7, 8, 12, 15, 22], "oracle_map": [], "orang": 30, "order": 3, "origin": [6, 8, 12], "origin_col": 6, "oserror": [], "osm": 12, "osm_typ": [12, 30], "osmid": 30, "osmnx": 12, "other": [12, 30], "otherwis": [3, 7], "our": 23, "outer_box": [27, 28, 32], "outgo": 6, "outlier": [], "output": [7, 12, 15], "output_burst": 15, "output_cr": 8, "output_traj_col": [], "outsid": [], "over": 7, "overal": 7, "overlap": [3, 5, 12, 28, 30], "overlapping_visit": 5, "overpass": 30, "overrid": [7, 10], "overridden": [], "own": 7, "ox": 3, "p": 5, "pa": 30, "packag": [17, 25], "pad": [], "pad_short_stop": [], "page": 23, "pair": [3, 8, 15, 27], "panda": [3, 6, 7, 12, 15, 22, 23, 26, 27, 28, 30, 32], "parallel": [22, 23], "param": 6, "paramet": [5, 7, 8, 12, 15, 29, 32, 33], "pareto": 15, "park": [3, 12, 15, 30, 31], "park_ratio": 3, "parquet": [3, 15, 22, 23, 27, 28, 29, 32], "pars": [7, 15, 29], "parse_agent_attr": 15, "parse_d": [], "parser": [], "part": [22, 23], "partit": 15, "partition_bi": [], "partition_col": 15, "pass": 7, "passthrough": [], "passthrough_col": [], "path": [3, 15], "pattern": [3, 33], "pd": [3, 5, 7, 8, 12, 15, 26, 27, 28, 30, 32], "peek": [], "pennsylvania": [22, 23], "per": [6, 7, 10, 15], "percentag": 6, "perf_count": 31, "perform": [5, 10, 22], "period": [7, 33], "permit": 29, "persist": [3, 12, 31], "persist_block": 3, "persist_city_properti": 3, "philadelphia": 30, "philadelphia_build": 30, "philadelphia_street": 30, "pick": [], "pilesgrov": 30, "ping": [7, 8, 15, 23, 26, 29, 32], "pip": [22, 23], "pipe": [], "pipelin": [22, 23], "place": [], "place_buildings_in_block": 3, "plain": 32, "platform": [], "plausibl": [], "plot": [3, 15, 26, 30, 31], "plot_circl": [], "plot_citi": [3, 31], "plot_hexagon": [], "plot_od_map": [], "plot_p": 26, "plot_polygon": [], "plot_popul": 15, "plot_sparse_clust": [], "plot_stop": 26, "plot_stops_barcod": [26, 27, 28, 29, 32], "plot_time_barcod": [26, 27, 28, 29, 32], "plot_traj": 15, "plt": [26, 27, 28, 29, 30, 31, 32], "plu": [], "po": [], "poi": 23, "poi_data": 15, "poi_map": [], "poi_tabl": [], "point": [3, 7, 8, 23, 29, 30], "point_color": [], "point_in_polygon": [], "pointer": [], "points_gdf": [], "points\u00b2": [], "poisson": 15, "polici": [], "poly_cr": 7, "polygon": [3, 7, 8, 12, 30], "popul": [3, 12, 15], "portion": 10, "posit": [7, 15], "possibl": [15, 27], "post": [23, 28, 32], "postcod": 30, "postprocess": [28, 32], "pre": [], "precis": 5, "precision_recall_f1_from_minut": 5, "predefin": 3, "predict": 5, "preferenti": 15, "prepar": [], "presenc": 10, "present": [7, 12], "preserv": 33, "previou": [22, 23], "print": [7, 15, 26, 30, 31], "print_stop": [], "prioriti": 12, "privaci": [6, 23, 33], "probabl": 15, "process": [12, 15, 22, 23, 28, 30, 32, 33], "produc": 5, "product": [22, 23], "profession": 12, "progress": 26, "project": [3, 7, 8, 12, 22, 23, 27, 29], "promin": 12, "propag": [], "proper": [10, 30], "properti": [3, 15], "proport": [3, 7], "protocol": [], "provid": [3, 7, 8, 10, 12, 15, 22, 23, 27], "proxim": 23, "public": [], "purpl": 30, "purpos": 33, "put": [], "py": [], "pyarrow": 15, "pydeck": [], "pyfunc": [], "pyplot": [26, 27, 28, 29, 30, 31, 32], "pyproj": [], "pyspark": [22, 23], "python": [22, 23], "q": 7, "q_filter": 7, "q_rang": [], "q_stat": 7, "qbar": 7, "qualifi": 29, "qualiti": 22, "quantil": [], "queri": 12, "r": 5, "radiu": [15, 23, 26, 28, 33], "rais": [3, 5, 7, 8], "random": 15, "randomcitygener": 3, "rang": 7, "rank": [], "rate": 15, "rather": 15, "raw": [], "rawio": [], "rawiobas": [], "rb": [], "re": [7, 26, 27], "reach": [], "read": [22, 23], "read1": [], "read_csv": [], "read_data": 22, "read_fil": [26, 27, 28, 32], "readabl": [], "readal": [], "reader": [], "readi": [22, 23], "readinto": [], "readinto1": [], "readlin": [], "real": 33, "reassign": [], "recal": 5, "recent": [], "recogn": [], "recomput": 10, "reconfigur": [], "record": 7, "recurs": [], "red": [26, 32], "reduc": 12, "ref": 30, "refer": 8, "regardless": [], "region_gdf": [], "regist": [], "reiniti": 15, "rel": [6, 7], "relat": 23, "relev": [10, 15], "reli": 32, "remain": 6, "remov": [7, 12, 30], "remove_overlap": [12, 28, 30, 32], "repeat": 15, "repeatedli": [], "replac": 15, "replace_sparse_traj": 15, "replic": [22, 23], "report": 23, "repres": [3, 15, 29], "reproduc": [6, 15], "reproject": 15, "reproject_to_merc": 15, "request": 12, "requir": [3, 7, 27, 32], "research": [22, 23], "reset": 15, "reset_trajectori": 15, "resid": 15, "residenti": [22, 30], "resolut": 7, "resolv": 10, "resourc": [22, 23, 31], "respect": [], "result": [5, 7, 12, 26, 29, 30], "results_df": 26, "retail": [3, 15, 30, 31], "retail_ratio": 3, "retain": [7, 8], "retriev": [3, 12], "return": [5, 6, 7, 8, 12, 15, 23], "return_cor": [], "revers": 30, "rgba": [], "right": [5, 31], "riverwalk": 30, "rn": [], "road": 30, "roam": 29, "robust": 7, "rog": [], "rog_spark": [], "roster": 15, "rotat": 12, "rotate_streets_to_align": 12, "rotated_streets_gdf": 12, "rotation_deg": 12, "rotation_degre": 12, "routin": [], "row": [3, 6, 7, 10, 12], "rt": [], "rule": [], "run": [15, 22, 23, 26], "runtim": 33, "runtime_data": 26, "runtime_df": 26, "s2": [7, 23], "s3": 15, "s3f": 15, "s3filesystem": 15, "salem": 30, "salem_build": 30, "salem_street": 30, "same": [15, 22, 23, 27, 32], "sampl": [7, 15, 22, 30, 33], "sample_from_fil": [26, 27, 28, 29, 32], "sample_hier_nhpp": 15, "sample_step": 15, "sample_traj_hier_nhpp": 15, "sample_trajectori": 15, "sample_us": [], "sampler": 23, "satisfi": [], "saturdai": [], "save": [3, 15, 30], "save_geopackag": [3, 31], "save_pop": 15, "scalabl": 22, "scalar": 7, "scale": [3, 12, 15, 22, 23, 26], "scan": [], "schema": [12, 23, 30], "scikit": [22, 23], "sea": [22, 23], "seamless": 23, "search": 23, "second": [7, 26, 27, 32], "section": 17, "see": [6, 23], "seed": [3, 6, 15], "seek": [], "seek_cur": [], "seek_end": [], "seek_set": [], "seekabl": [], "segment": 33, "select": [10, 28, 33], "select_hom": 10, "select_most_stable_clust": [], "select_workplac": [], "self": 15, "semant": 7, "sep": [], "separ": [], "sequenc": 29, "sequenti": [23, 29, 33], "seri": [3, 7, 8], "serv": [], "servic": [12, 30], "session": 8, "set": [5, 6, 12, 15, 32], "set_aspect": 30, "set_axis_off": [], "set_cr": 31, "set_osmnx_cache_mod": 12, "set_titl": [26, 30], "set_xlabel": 26, "set_xlim": [26, 27, 28, 29, 32], "set_ylabel": 26, "setup": 25, "sever": [], "shape": [3, 7, 8, 12, 22, 27, 28, 29, 31, 32], "share": [6, 7], "shift": [], "short": [], "shorter": [], "shortest": 3, "shortest_path": 3, "shorthand": 7, "should": 29, "show": [26, 27, 28, 29, 30, 31, 32], "shown": 33, "shp_plt": [], "shuffl": [], "side": 3, "signatur": [], "significantli": [], "similar": [], "simpl": 22, "simpli": [], "simplifi": 12, "simul": 15, "simulate_traj": 15, "simultan": [], "sinc": 7, "singl": [3, 5, 6, 7, 10, 12, 15, 22, 23], "size": [3, 7, 12, 26, 29], "slice": 10, "slice_datetimes_interval_fast": [], "small": 31, "smallest": [], "snow_engin": [], "so": [7, 15], "socket": [], "softwar": [22, 23], "sole": [], "some": [], "sort": [], "sort_tim": [], "sourc": [3, 5, 6, 7, 8, 10, 12, 15, 22, 23], "spam": [], "span": 7, "spark": [8, 23], "spark_sess": 8, "sparksess": 8, "spars": [15, 33], "sparse_path": 15, "sparse_traj": 15, "sparsifi": 15, "spatial": [7, 8, 12, 22, 23, 26, 27, 32, 33], "specif": [], "specifi": [3, 7, 8, 32], "speed": 15, "spend": 29, "spent": [3, 15], "split": 5, "split_fract": 5, "spring": 30, "spurious": [], "sql": [], "sqlalchemi": [], "st_hdbscan": [26, 28], "st_hdbscan_per_us": [], "stabil": 28, "stagger": [], "stai": [15, 29], "standard": 7, "start": [3, 5, 7, 8, 15, 26], "start_": [], "start_col": [], "start_coord": 3, "start_datetim": 10, "start_tim": 8, "start_timestamp": 5, "stat": [], "state": [15, 30], "statement": [], "static": 15, "stationari": 33, "statist": [7, 10], "step": [12, 15], "step_se": 15, "still": [15, 29], "still_prob": 15, "stop": [5, 10, 22, 23], "stop_color": [27, 28, 29, 32], "stop_data": [], "stop_detect": [22, 26, 27, 28, 29, 32], "stop_tabl": [], "stops_gb": 27, "stops_hdb": 28, "stops_output": 26, "stops_subset": 26, "stops_tabl": 10, "stops_tadb": 32, "store": [3, 15], "str": [3, 5, 6, 7, 8, 15], "str_from_tim": 7, "strategi": [], "stratif": 23, "stream": [], "street": [3, 12, 30, 31], "street_adjacency_edg": 3, "street_graph": 3, "street_spac": 3, "streets_df": 3, "streets_gdf": [3, 12, 31], "streets_path": 3, "strftime": [], "strict": [], "strictli": 10, "string": [3, 7, 15], "stringio": [], "strip": [], "structur": 3, "style": [], "subclass": [], "submodul": 25, "subplot": [26, 27, 28, 29, 30, 31, 32], "subset": [8, 26, 30], "subtyp": [12, 30], "subtype_2": 30, "subtype_3": 30, "suit": [], "suitabl": [7, 22, 23], "sum": 6, "summar": [], "summari": [], "summarize_stop": [], "summarize_stop_grid": [], "sundai": [], "suppli": 5, "support": [8, 27], "suptitl": [28, 29], "surfac": 12, "surviv": 7, "switch": [], "symmetr": [], "syneth": 31, "synthet": [22, 23], "synthetic_poi": 31, "system": 8, "systemat": 3, "t": 26, "t0": [26, 31], "ta": [26, 32], "ta_dbscan": [26, 32], "ta_dbscan_label": 32, "ta_dbscan_per_us": [], "tabl": [5, 6, 15], "table_column": [], "table_column_filt": [], "table_column_uniqu": [], "table_nam": [], "tag": 12, "take": [8, 15], "target": 8, "tc": [27, 28, 29, 32], "team": 12, "tell": [], "temp": 12, "tempor": [22, 23, 26, 27, 32, 33], "temporari": [], "termin": [], "tessel": [23, 33], "test": [], "text": 26, "textio": [], "textiobas": [], "textiowrapp": [], "th": 15, "than": [15, 29], "thei": 8, "them": [8, 12, 32], "thi": [3, 7, 8, 10, 12, 15, 17, 26, 29, 32], "those": [8, 12, 28], "though": [], "threshold": 7, "through": [3, 10, 27], "ti": [], "tick": 15, "tight_layout": [26, 27, 29, 30, 32], "tile": 12, "time": [3, 7, 12, 15, 23, 26, 29, 31, 32, 33], "time_col": 15, "time_thresh": [26, 27, 28, 32], "time_threshold": 33, "time_valu": 7, "time_weight": [], "timedelta": 26, "timefram": 8, "timestamp": [5, 7, 8, 15, 26, 27, 28, 29, 32], "timezon": [7, 8], "timezone_offset": 7, "titl": [27, 32], "to_cr": [27, 28, 32], "to_dict": 30, "to_fil": [3, 30], "to_geodatafram": 3, "to_merc": 3, "to_project": [7, 8, 32], "to_rgba": [], "to_str": 26, "to_tessel": [7, 26, 27], "to_timestamp": 7, "to_yyyymmdd": 7, "to_zoned_datetim": 7, "todo": 8, "togeth": [], "tolist": 12, "too": [], "tool": 23, "top": [], "top_k_destin": [], "top_k_origin": [], "total": [5, 6, 7, 15, 26], "total_bound": [27, 28, 32], "total_dur": [], "total_pr": 5, "total_truth": 5, "toyama": 29, "tp": 5, "trace": 22, "trackintel": [22, 23], "traj": [8, 15, 26, 27, 28, 29, 32], "traj_clean": 26, "traj_col": [5, 7, 8, 10, 15, 27, 28, 29, 32], "traj_from_dest_diari": 15, "traj_subset": 26, "trajectori": [7, 8, 15, 22, 23, 26, 33], "transform": [12, 15, 22], "translat": [], "transpar": 15, "travel": [15, 23], "tree": [], "trigger": [], "trip": [6, 22, 33], "true": [3, 7, 12, 15, 26, 27, 28, 29, 30, 31, 32], "true_visit": 5, "truncat": 12, "truth": [5, 15], "try": [], "ts_seri": [], "tty": [], "tunnel": [12, 30], "tupl": [3, 12, 31], "tutori": 27, "twilight": [], "two": [3, 12, 29, 32], "two_dai": 26, "txt": [], "type": [3, 5, 6, 7, 8, 10, 12, 15, 31], "typic": [], "tz": [15, 27], "tz_offset": 7, "u": 3, "uid": [], "unchang": [], "underli": [], "underneath": [], "understand": 33, "uniform": 33, "uniqu": [3, 5, 7, 26], "unit": [7, 12], "univers": [22, 23, 30], "unix": [7, 27], "unix_t": [27, 28, 29, 32], "unknown": 12, "unless": [], "unlik": 32, "unspecifi": [], "unsupportedoper": [], "until": [3, 15], "untransl": [], "unus": [], "unweight": [], "up": [], "updat": [3, 12, 15], "upenn": [22, 23], "upon": [], "upper": [], "upper_quantil": [], "us": [3, 7, 8, 12, 15, 26, 27, 30, 32, 33], "usabl": [], "use_datetim": [], "use_offset": [], "user": [5, 7, 8, 10, 15, 26, 27, 28, 29, 32], "user_data_hdb": 28, "user_data_tadb": 32, "user_id": [5, 7, 8, 27, 28, 29, 32], "userwarn": [], "usr_polygon": [], "usual": [], "utc": 7, "utc_timestamp": 7, "util": 12, "v": [3, 26], "va": 26, "val": 26, "valid": [12, 22, 23], "valu": [3, 6, 7, 15, 28], "value_count": 30, "valueerror": [3, 5, 7, 8], "vari": [], "variabl": [15, 33], "variat": 33, "variou": [5, 22], "vector": 3, "verbos": [3, 7, 15], "version": [], "via": 7, "view": 7, "visit": [22, 23], "viz": [26, 27, 28, 29, 32], "w": 7, "w_min": [], "wa": [], "wai": [], "walnut": 30, "warn": [7, 26], "watt": [22, 23], "we": [26, 32], "web": [3, 12, 15], "web_mercator_origin": 3, "web_mercator_origin_i": 3, "web_mercator_origin_x": [3, 15], "websit": 23, "week": 7, "weekdai": [], "weekli": 7, "weight": [], "weight_col": 6, "weight_freq": [], "well": 27, "what": 10, "when": [6, 7, 12, 15, 27], "whenc": [], "where": [3, 5, 6, 7, 15, 31], "whether": [3, 15], "which": [3, 7, 15, 27, 29, 33], "while": [], "white": [], "who": 8, "whole": 15, "whose": [3, 6, 7], "width": 3, "window": [7, 10, 23, 26], "within": [3, 7, 8, 12, 15, 29, 32], "without": [3, 22, 23], "wkt": 7, "woodstown": 30, "work": [3, 15, 27, 31, 32], "work_dat": [], "work_end_hour": [], "work_ratio": 3, "work_start_hour": [], "workdai": [], "workday_stop": [], "workplac": [15, 22, 23, 30, 31], "workstat": [22, 23], "would": [], "wrap": [], "wrapper": 30, "writabl": [], "write": 3, "write_through": [], "writeabl": [], "writelin": [], "writer": [], "written": [], "wt": [], "x": [3, 7, 8, 12, 15, 26, 27, 28, 29, 32], "x8": 3, "x_block": 12, "x_mercat": 12, "y": [3, 7, 8, 12, 15, 27, 28, 29, 32], "y8": 3, "ye": [12, 30], "yield": [], "your": 22, "youthful_may": 26, "yyyi": [], "yyyymmdd": 7, "z": 3, "zero": [], "zip": 26, "zone": 7, "zoned_datetime_from_ts_and_offset": [], "zorder": 3, "\u03b5": [6, 15], "\u03b5_x": 15, "\u03b5_y": 15, "\u03c3": 15, "\u03c3\u00b2": 15}, "titles": ["Aggregation & Debiasing", "agg", "aggregation", "city_gen", "constants", "contact_estimation", "displacement", "filters", "filters_spark", "generation", "home_attribution", "io", "map_utils", "metrics", "stop_detection", "traj_gen", "visit_attribution", "API Reference", "base_module package", "Co-location & Contact Networks", "Data Ingestion", "Filtering & Completeness", "Getting Started", "NOMAD: Network for Open Mobility Analysis and Data", "Mobility Metrics", "src", "Benchmarking Stop Detection Algorithms", "Grid-Based Stop Detection", "HDBSCAN Stop Detection", "Lachesis Stop Detection", "Downloading places of interest (POI) Data from OSM", "Generating synthetic places of interest (POI) Data", "TADBSCAN Stop Detection", "Stop Detection", "Synthetic Data Generation", "Tessellation", "Visit Attribution"], "titleterms": {"With": 22, "agg": 1, "aggreg": [0, 2], "algorithm": 26, "analysi": 23, "api": 17, "attribut": 36, "base": [27, 33], "base_modul": 18, "basic": 22, "benchmark": 26, "bound": 30, "box": 30, "citi": 30, "city_gen": 3, "co": 19, "commun": 23, "comparison": 26, "complet": 21, "constant": 4, "contact": 19, "contact_estim": 5, "content": 18, "core": [], "data": [20, 23, 30, 31, 34], "dbscan": 33, "debias": 0, "detect": [26, 27, 28, 29, 32, 33], "displac": 6, "doc_test": 18, "download": 30, "exampl": 22, "featur": 22, "filter": [7, 21], "filters_spark": 8, "from": 30, "gener": [9, 31, 34], "get": 22, "grid": [27, 33], "grid_bas": [], "hdbscan": [28, 33], "home_attribut": 10, "i": 22, "indic": 23, "ingest": 20, "instal": [22, 23], "interest": [30, 31], "io": 11, "kei": 22, "lachesi": [29, 33], "licens": 22, "link": 23, "locat": 19, "map_util": 12, "method": 33, "metric": [13, 24], "mobil": [23, 24], "modul": [17, 18, 23], "name": 30, "network": [19, 23], "next": 22, "nomad": [22, 23], "od": [], "open": 23, "osm": 30, "overal": 26, "overview": [23, 33], "packag": 18, "paramet": 3, "perform": 26, "place": [30, 31], "poi": [30, 31], "postprocess": [], "preprocess": [], "privaci": [], "process": [], "quick": [22, 23], "refer": 17, "requir": 22, "return": 3, "runtim": 26, "scalabl": 26, "sequenti": [], "setup": [18, 26], "snowflak": [], "spark": 22, "sparsiti": [], "src": 25, "start": 22, "step": 22, "stop": [26, 27, 28, 29, 32, 33], "stop_detect": 14, "submodul": 18, "support": [22, 23], "synthet": [31, 34], "tabl": 23, "tadbscan": 32, "tessel": 35, "traj_gen": 15, "util": [], "visit": 36, "visit_attribut": 16, "visual": 26, "viz": [], "what": 22}})
\ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
index 3332079c..f51bcbc7 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -56,6 +56,7 @@ Installation
:caption: Examples
:hidden:
+ source/ingesting-data
source/benchmarking_of_stop_detection_algorithms
source/lachesis_demo
source/tadbscan_demo
diff --git a/docs/source/benchmarking_of_stop_detection_algorithms.ipynb b/docs/source/benchmarking_of_stop_detection_algorithms.ipynb
index 747c1e8c..64549ab4 100644
--- a/docs/source/benchmarking_of_stop_detection_algorithms.ipynb
+++ b/docs/source/benchmarking_of_stop_detection_algorithms.ipynb
@@ -2,73 +2,191 @@
"cells": [
{
"cell_type": "markdown",
- "id": "92838936",
+ "id": "67fc810a",
"metadata": {},
"source": [
- "# Benchmarking Stop Detection Algorithms\n",
- "\n",
- "This notebook compares the performance of four stop detection algorithms: **Lachesis**, **TA-DBSCAN**, **Grid-Based**, and **HDBSCAN**. We evaluate both overall runtime on a full trajectory dataset and how runtime scales with increasing data size."
+ "# Comparing runtimes of different stop detection algorithms on toy datasets"
]
},
{
"cell_type": "markdown",
- "id": "426e43f2",
+ "id": "f152187c",
"metadata": {},
"source": [
- "## Setup"
+ "Here we compare the runtimes of four different stop detection algorithms: Lachesis, grid-based, temporal DBSCAN, and HDBSCAN."
]
},
{
"cell_type": "code",
"execution_count": 1,
- "id": "74f96664",
+ "id": "474229df",
"metadata": {
"execution": {
- "iopub.execute_input": "2025-10-17T05:59:34.956525Z",
- "iopub.status.busy": "2025-10-17T05:59:34.956274Z",
- "iopub.status.idle": "2025-10-17T05:59:38.575372Z",
- "shell.execute_reply": "2025-10-17T05:59:38.574638Z"
+ "iopub.execute_input": "2025-11-24T18:33:19.946986Z",
+ "iopub.status.busy": "2025-11-24T18:33:19.946986Z",
+ "iopub.status.idle": "2025-11-24T18:33:23.251955Z",
+ "shell.execute_reply": "2025-11-24T18:33:23.251955Z"
}
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Lachesis execution time: 0.02721381187438965 seconds\n",
+ "TA-DBSCAN execution time: 0.012791156768798828 seconds\n",
+ "TA-DBSCAN clustering time: 0.009821414947509766 seconds\n",
+ "TA-DBSCAN post-processing time: 0.0029697418212890625 seconds\n",
+ "Grid-Based execution time: 0.022524595260620117 seconds\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "HDBSCAN execution time: 0.3206779956817627 seconds\n",
+ "HDBSCAN clustering time: 0.3206779956817627 seconds\n",
+ "HDBSCAN post-processing time: 0.0 seconds\n"
+ ]
+ }
+ ],
"source": [
- "import time\n",
- "import warnings\n",
- "import pandas as pd\n",
- "import geopandas as gpd\n",
+ "%matplotlib inline\n",
+ "import matplotlib\n",
+ "matplotlib.use('TkAgg')\n",
"import matplotlib.pyplot as plt\n",
+ "plt.ion()\n",
+ "\n",
+ "# Imports\n",
"import nomad.io.base as loader\n",
- "import nomad.filters as filters\n",
- "from nomad.stop_detection.viz import plot_pings, plot_stops, plot_time_barcode, plot_stops_barcode, clip_spatial_outliers\n",
- "import nomad.stop_detection.lachesis as LACHESIS\n",
+ "import geopandas as gpd\n",
+ "from shapely.geometry import box\n",
+ "import pandas as pd\n",
+ "import numpy as np\n",
+ "from nomad.stop_detection.viz import plot_stops_barcode, plot_pings, plot_stops, plot_time_barcode\n",
"import nomad.stop_detection.dbscan as DBSCAN\n",
+ "import nomad.stop_detection.lachesis as LACHESIS\n",
"import nomad.stop_detection.grid_based as GRID_BASED\n",
"import nomad.stop_detection.hdbscan as HDBSCAN\n",
+ "import nomad.filters as filters \n",
+ "import nomad.stop_detection.postprocessing as post\n",
+ "import time\n",
+ "from tqdm import tqdm\n",
+ "\n",
+ "# Load data\n",
+ "import nomad.data as data_folder\n",
+ "from pathlib import Path\n",
+ "data_dir = Path(data_folder.__file__).parent\n",
+ "city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')\n",
+ "outer_box = box(*city.total_bounds).buffer(15, join_style='mitre')\n",
+ "\n",
+ "filepath_root = 'gc_data_long/'\n",
+ "tc = {\n",
+ " \"user_id\": \"gc_identifier\",\n",
+ " \"timestamp\": \"unix_ts\",\n",
+ " \"x\": \"dev_x\",\n",
+ " \"y\": \"dev_y\",\n",
+ " \"ha\":\"ha\",\n",
+ " \"date\":\"date\"}\n",
+ "\n",
+ "users = ['admiring_brattain']\n",
+ "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)\n",
+ "\n",
+ "# Lachesis (sequential stop detection)\n",
+ "start_time = time.time()\n",
+ "stops = LACHESIS.lachesis(traj, delta_roam=20, dt_max = 60, dur_min=5, complete_output=True, keep_col_names=True, traj_cols=tc)\n",
+ "execution_time_lachesis = time.time() - start_time\n",
+ "print(f\"Lachesis execution time: {execution_time_lachesis} seconds\")\n",
+ "\n",
+ "# Density based stop detection (Temporal DBSCAN)\n",
+ "start_time = time.time()\n",
+ "user_data_tadb = traj.assign(cluster=DBSCAN.ta_dbscan_labels(traj, time_thresh=240, dist_thresh=15, min_pts=3, traj_cols=tc))\n",
+ "clustering_time_tadbscan = time.time() - start_time\n",
+ "start_time_post = time.time()\n",
+ "cluster_labels_tadb = post.remove_overlaps(user_data_tadb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)\n",
+ "execution_time_tadbscan = time.time() - start_time\n",
+ "post_time_tadbscan = time.time() - start_time_post\n",
+ "print(f\"TA-DBSCAN execution time: {execution_time_tadbscan} seconds\")\n",
+ "print(f\"TA-DBSCAN clustering time: {clustering_time_tadbscan} seconds\")\n",
+ "print(f\"TA-DBSCAN post-processing time: {post_time_tadbscan} seconds\")\n",
+ "\n",
+ "# Grid-based\n",
+ "start_time = time.time()\n",
+ "traj['h3_cell'] = filters.to_tessellation(traj, index=\"h3\", res=10, traj_cols=tc, data_crs='EPSG:3857')\n",
+ "stops_gb = GRID_BASED.grid_based(traj, time_thresh=240, complete_output=True, traj_cols=tc, location_id='h3_cell')\n",
+ "execution_time_grid = time.time() - start_time\n",
+ "print(f\"Grid-Based execution time: {execution_time_grid} seconds\")\n",
"\n",
- "city = gpd.read_file(\"../../examples/garden_city.geojson\")\n",
- "traj = loader.sample_from_file('../../examples/gc_data/', users=['youthful_mayer'], format='csv')"
+ "# HDBSCAN\n",
+ "start_time = time.time()\n",
+ "user_data_hdb = traj.assign(cluster=HDBSCAN.hdbscan_labels(traj, time_thresh=240, min_pts=3, min_cluster_size=2, traj_cols=tc))\n",
+ "clustering_time_hdbscan = time.time() - start_time\n",
+ "start_time_post = time.time()\n",
+ "cluster_labels_hdb = post.remove_overlaps(user_data_hdb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3) \n",
+ "execution_time_hdbscan = time.time() - start_time\n",
+ "post_time_hdbscan = time.time() - start_time_post\n",
+ "print(f\"HDBSCAN execution time: {execution_time_hdbscan} seconds\")\n",
+ "print(f\"HDBSCAN clustering time: {clustering_time_hdbscan} seconds\")\n",
+ "print(f\"HDBSCAN post-processing time: {post_time_hdbscan} seconds\")"
]
},
{
"cell_type": "markdown",
- "id": "cb276fd9",
+ "id": "c88f426d",
"metadata": {},
"source": [
- "## Overall Runtime Comparison\n",
- "\n",
- "We first measure the total execution time for each algorithm on the complete dataset."
+ "## Summary of Single-User Performance"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6a678431",
+ "metadata": {},
+ "source": [
+ "### Lachesis"
]
},
{
"cell_type": "code",
"execution_count": 2,
- "id": "19184dee",
+ "id": "b7480c93",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:23.251955Z",
+ "iopub.status.busy": "2025-11-24T18:33:23.251955Z",
+ "iopub.status.idle": "2025-11-24T18:33:23.475346Z",
+ "shell.execute_reply": "2025-11-24T18:33:23.475346Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),\n",
+ " gridspec_kw={'height_ratios':[10,1]})\n",
+ "\n",
+ "gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')\n",
+ "city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')\n",
+ "\n",
+ "plot_stops(stops, ax=ax_map, cmap='Reds')\n",
+ "plot_pings(traj, ax=ax_map, s=6, point_color='black', cmap='twilight', traj_cols=tc)\n",
+ "ax_map.set_axis_off()\n",
+ "\n",
+ "plot_time_barcode(traj[tc['timestamp']], ax=ax_barcode, set_xlim=True)\n",
+ "plot_stops_barcode(stops, ax=ax_barcode, cmap='Reds', set_xlim=False, timestamp='unix_ts')\n",
+ "\n",
+ "plt.tight_layout(pad=0.1)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "98cdda1f",
"metadata": {
"execution": {
- "iopub.execute_input": "2025-10-17T05:59:38.579313Z",
- "iopub.status.busy": "2025-10-17T05:59:38.578920Z",
- "iopub.status.idle": "2025-10-17T05:59:58.677680Z",
- "shell.execute_reply": "2025-10-17T05:59:58.676969Z"
+ "iopub.execute_input": "2025-11-24T18:33:23.475346Z",
+ "iopub.status.busy": "2025-11-24T18:33:23.475346Z",
+ "iopub.status.idle": "2025-11-24T18:33:23.482904Z",
+ "shell.execute_reply": "2025-11-24T18:33:23.482904Z"
}
},
"outputs": [
@@ -76,189 +194,214 @@
"name": "stdout",
"output_type": "stream",
"text": [
- " Algorithm Runtime (s)\n",
- " Lachesis 1.060303\n",
- " TA-DBSCAN 6.314632\n",
- "Grid-Based 0.207118\n",
- " HDBSCAN 12.504672\n"
+ "Summary of Single-User Performance\n",
+ "Lachesis execution time: 0.02721381187438965 seconds\n",
+ "TA-DBSCAN execution time: 0.012791156768798828 seconds\n",
+ "Grid-Based execution time: 0.022524595260620117 seconds\n",
+ "HDBSCAN execution time: 0.3206779956817627 seconds\n"
]
}
],
"source": [
- "algorithms = [\n",
- " ('Lachesis', lambda t: LACHESIS.lachesis(t, delta_roam=20, dt_max=60, dur_min=5, \n",
- " complete_output=True, keep_col_names=True,\n",
- " latitude=\"latitude\", longitude=\"longitude\")),\n",
- " ('TA-DBSCAN', lambda t: DBSCAN.ta_dbscan(t, time_thresh=240, dist_thresh=15, min_pts=3, \n",
- " dur_min=5, complete_output=True,\n",
- " latitude=\"latitude\", longitude=\"longitude\")),\n",
- " ('Grid-Based', lambda t: GRID_BASED.grid_based(\n",
- " t.assign(h3_cell=filters.to_tessellation(t, index=\"h3\", res=10, \n",
- " latitude='latitude', longitude='longitude', \n",
- " data_crs='EPSG:4326')),\n",
- " time_thresh=240, complete_output=True, timestamp='timestamp', location_id='h3_cell')),\n",
- " ('HDBSCAN', lambda t: HDBSCAN.st_hdbscan(t, time_thresh=240, min_pts=3, min_cluster_size=2, \n",
- " dur_min=5, complete_output=True,\n",
- " latitude=\"latitude\", longitude=\"longitude\"))\n",
- "]\n",
- "\n",
- "results = []\n",
- "for name, func in algorithms:\n",
- " t0 = time.time()\n",
- " stops_output = func(traj)\n",
- " results.append({'Algorithm': name, 'Runtime (s)': time.time() - t0})\n",
- " if name == 'Lachesis':\n",
- " stops = stops_output\n",
- "\n",
- "results_df = pd.DataFrame(results)\n",
- "print(results_df.to_string(index=False))"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "5b150f1e",
- "metadata": {},
- "source": [
- "## Visualization\n",
- "\n",
- "Spatial and temporal visualization of detected stops using the Lachesis algorithm."
+ "print(\"Summary of Single-User Performance\")\n",
+ "print(f\"Lachesis execution time: {execution_time_lachesis} seconds\")\n",
+ "print(f\"TA-DBSCAN execution time: {execution_time_tadbscan} seconds\")\n",
+ "print(f\"Grid-Based execution time: {execution_time_grid} seconds\")\n",
+ "print(f\"HDBSCAN execution time: {execution_time_hdbscan} seconds\")"
]
},
{
"cell_type": "code",
- "execution_count": 3,
- "id": "f9c852d1",
+ "execution_count": 4,
+ "id": "41e9a154",
"metadata": {
"execution": {
- "iopub.execute_input": "2025-10-17T05:59:58.680965Z",
- "iopub.status.busy": "2025-10-17T05:59:58.680723Z",
- "iopub.status.idle": "2025-10-17T05:59:59.277513Z",
- "shell.execute_reply": "2025-10-17T05:59:59.276740Z"
+ "iopub.execute_input": "2025-11-24T18:33:23.482904Z",
+ "iopub.status.busy": "2025-11-24T18:33:23.482904Z",
+ "iopub.status.idle": "2025-11-24T18:33:23.492088Z",
+ "shell.execute_reply": "2025-11-24T18:33:23.492088Z"
}
},
"outputs": [
{
- "data": {
- "image/png": "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FhFRB98YL5fo1nz9TZvnnbN9Fxw1TgZNRO2EexYRUQfz+gP4v/c/RF19251tkvuYXlu4DHUeL59YJGonDJqIiE7DJbpyRzX+seD9NuspfdFHn2FfYbHKm4i6YeeWycnJWPT26+jKEuK775vYiaj9fLbmC/zql/+FTXsKcOtN075VXqu/2oD12/Pw3tJl6Nu3b8N4q9WK8ReOQ1duw+VSY3u/S4/tOHWJoMnhcGD2nfegK8t64x+nexWIqAuy2+0orHDhiRdexYHCQ5g1bUqr70WSs1Sr/70Bf359Puy9+mHuLx/u0PapI9pwCcpm3PrDdl0G23Eyiq9RISI6TWxRdsT3HYQ3l61C7u69mHv7bAwa0M/QvJXVNXg1azHWZucipvcA9Y4/ImpfDJqIiE4jmy0KSf3TsPVACe5++He4ZNx5mHLJJIwaMQzR9uYv3vX7/cg7UIjP1m3Eqq82IGSJRmL/NJh5HxNRh2DQRER0mslluaSUfggGAvhi6178e8t2hPx+9O+bgl6JCTCbTKhx1aKkvAImswVBsx1xKakq4CKijsOgiYiok7BYrejVR788FwwGUe31oLLcpYblbFJc3zSVhohOD+59RESdkHQdYImNO92rQURNsJ8mIiIiIgMYNBEREREZwKCJiIiIyAAGTUREREQGMGgiIiIiMoBBExEREZEBDJqIiIiIDGDQRERERGQAgyYiIiIiAxg0ERERERnAoImIiIjIAAZNRERERAaYNE3TjCQkIiIi6sl4pomIiIjIAAZNRERERAYwaCIiIiIygEETERERkQEMmoiIiIgMYNBEREREZACDJiIiIiIDGDQRERERGcCgiYiIiMgABk1EREREBjBoIiIiIjKAQRMRERGRAQyaiIiIiAxg0ERERERkAIMmIiIiIgMYNBEREREZwKCJiIiIyAAGTUREREQGMGgiIiIiMoBBExEREZEBDJqIiIiIDGDQRERERGQAgyYiIiIiAxg0ERERERnAoImIiIjIAAZNRERERAYwaCIiIiIygEETERERkQEMmoiIiIgMYNBEREREZACDJiIiIiIDGDQRERERGcCgiYiIiMgABk1EREREBjBoIiIiIjKAQRMRERGRAQyaiIiIiAxg0ERERERkAIMmIiIiIgMYNBEREREZwKCJiIiIyAAGTUREREQGMGgiIiIiMoBBExEREZEBDJqI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EXV00HTfffe1S9rOlHdrdZb1Zpl07Hq0RmfZxvbOu73Wo7XpO1Pe7bUerU3fmfLuLOvRWfJurc6y3vd1ojJpU3JPExnj8Xi0xx57TH2SjmXSEsukJZZJSyyTllgmzbE8Ol+ZGLoRnIiIiKin4z1NRERERAYwaCIiIiIygEETERERkQEMmoiIiIgMYNBEREREZEC3D5r+9re/YejQoYiOjsaECROwcePGZtNLS0vxgx/8AAMGDEBcXBzOP/98LF682FDe69evV72YXnfddS2mHThwQL1yRqYXFxc3m1ZSUgKr1aqmS7rOVi5VVVWYO3eu6qI+JiYG6enpuP/++1FTU3PCPC+//HK1TfJP8h01ahReeumlhulvvPGGmnb22We3mHfhwoVqmqxTd6ord955Z0OZREVFISMjA08++SQC4ZeYrlmzRk1LTk6GxyMv7W0krxSIzNtd6kjE/Pnz1b5xrL5ZumqZ9NT2hG1JS2xLunF7onVjWVlZWlRUlPbaa69pO3bs0O6++26tV69e2pEjRxrSXHXVVdqFF16off3111p+fr721FNPaWazWdu0adNJ87/rrru0Bx54QIuPj9eKi4ubTdu/f7905aClpaVpv//975tNe+aZZ7T09HQ1XdJ1tnLZtm2bdvPNN2vLli3T9u3bp3366afaiBEjtOnTp58w38suu0zlVVJSospS+tKQbXznnXfU9Ndff12Li4vTevfura1bt67ZvFOmTFFlMmTIEK071ZU77rhDu+aaa1SZHDhwQHvppZc0k8nUUCc+//zzhnoSKaeIe++9t6GedJc6EnHllVdqv/rVr7Tk5GStvr6+2bSuWiY9sT1hW9IS25Lu3Z5066Bp/Pjx2n333dcwHAwGtdTUVNXIRMiO99ZbbzWbT3bEV1999YR5u1wu1bjt3r1bmzVrlvb0008fs5F75JFH1Jff1MiRI7VHH330tAVNRsrlaO+++66q9H6//4QNnTT6Tcm2z549u6GhS0pK0n72s59pP/7xjxvSFBUVaXa7XVX609XQtVddkYbuxhtvbDZOGsyJEyc226GlnkyePLkhjdvtVmUVqSfdpY6IgoICLSYmRquurtYmTJigvf32282md+Uy6WntCduSltiWdO/2pNtenvP5fPjmm28wefLkhnHycj8ZltPgEZmZmViwYIE6PRgKhZCVlaVO4cnp4RN59913cdZZZ6nTibfddhtee+01KfUW6W644QY4HA589dVXalg+Zfj6669HZy6Xo8lpUnmZolwGaA051SrLbOpHP/qRKj+3291wqv2aa65B//790R3ripEykVP1X375JQoLC9WwnKqXU9ly2r671ZHXX39dXYJKSkpS+86//vWvY6brimXSk9oTtiUtsS3p/u1Jtw2aKioqEAwGW+w8MizXkyNkh/P7/UhJSYHdbse9996LJUuWqOvFJyJfjHxBQnZS+YLXrl3bIp3NZmtoBIV8yrCM78zlcvQ8Tz31FO655x7Dy5FlzJs3D1u3bsUVV1zRbNrYsWMxbNgwLFq0SB0YpKGTxq+71pUI2dbVq1dj1apVLcqkX79+uPbaa1VZROrJ6SqT9qwjcoCQbYzsO7Nnz1YH/v3797dI2xXLpCe1J2xLWmJb0v3bk24bNBn16KOPorq6WlXAnJwc/OIXv8DMmTOxbdu2486zZ88edRPbnDlz1LBEwrNmzTpuhCtfjNycKBVEPk9nBW4tp9Oponi5EfPxxx8/aXq5WTM+Pl79Arr77rvx85//HD/5yU9apJMykF8IcmCoq6vD1KlT0R3rilixYoUqE7kBUnZaqSvHKkspE9mhCwoK1C+wW2+9Fd2tjnzyySfNvu8+ffrgqquuaggCunqZsD05PrYljdiWdN32pHXnR7sQKTy5m/7IkSPNxsuwPLEg8vPz8eKLL2L79u0YPXq0GnfuueeqU3hyp/8rr7xyzLylMZMnFlJTU5tF/vKLQfKT04RNjRkzRp16l0ZRnvY455xzsGXLFnTWcolwuVzqV29CQoL6FWTk16xUwocfflg1dAMHDlSnYY+X7sEHH1Q7hZw2be2p+q5SV8R3v/tdvPzyy+qJF6kzx9tWaQTll9Vdd92lLrfIr9DuVkdk35FLElI/mv5alLMITzzxRIv60pXKpKe1J2xLWmJb0v3bk257pkkq1QUXXIBPP/20WWHK8KRJk9Rw5Dr40QUrX7CkPRZp3N566y0899xzqqGK/MvNzVWVWB59PF6EK48+nu5fhUbKJRLtX3311Sr9smXL1C8bI6SBl1PMgwYNOm4jJ3r37q3uz5Bfh12hTE6lrkTII8VSJvIY7YkadJl2++23n/Z60l51pLKyEkuXLlX3bzTddzZv3qzuy/n444+7dJn0tPaEbUlLbEt6QHuidWPymKM8SfHGG29oO3fu1O655x71mGNpaama7vP5tIyMDO2SSy5Rj37Ko47PPvuseozzgw8+OGaeS5YsUXf0y536R3vwwQe1cePGNXvaZfPmzWpYngAoLy9veBJAxp/OLgdOVC41NTXqKYQxY8aoMpFHXCP/AoFAq554aSryxEvTpxcqKioahv/85z+f1seE27quHO+Jl2M92eFwONSw1+tV9SQUCjXUt9P1eH1b1xH5fgcOHNiwbU3NnDlTmzFjRpcuk57YnrAtaYltSfduT7p10CReeOEF1Q+DNEzy2OOGDRuaTc/Ly1P9Q/Tr10+LjY3VvvOd77R4FLSpadOmaVOnTj3mNNkBpPBzc3NbNHJHO51B08nKJVLJjvXvROvb2obuaKezoWuPunIqDd3RTmdD19Z1RBrEn/70p8ectmDBArUcacy6apn01PaEbUlLbEu6b3tikv9O7RwVERERUc/Rbe9pIiIiImpLDJqIiIiIDGDQRERERGQAgyYiIiIiAxg0ERERERnAoImIiIjIAAZNRERERAYwaCIiIiIygEETERERkQEMmoiIiIgMYNBEREREhJP7f8qDKdHxyAyGAAAAAElFTkSuQmCC",
- "text/plain": [
- "
"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Runtime Disaggregation\n",
+ "Lachesis clustering time: 0.02721381187438965 seconds\n",
+ "--------------------------------\n",
+ "TA-DBSCAN clustering time: 0.009821414947509766 seconds\n",
+ "TA-DBSCAN post-processing time: 0.0029697418212890625 seconds\n",
+ "--------------------------------\n",
+ "Grid-Based clustering time: 0.022524595260620117 seconds\n",
+ "--------------------------------\n",
+ "HDBSCAN clustering time: 0.3206779956817627 seconds\n",
+ "HDBSCAN post-processing time: 0.0 seconds\n"
+ ]
}
],
"source": [
- "fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6, 6.5),\n",
- " gridspec_kw={'height_ratios': [10, 1]})\n",
- "\n",
- "two_days = 1704162819 + 3600*48\n",
- "traj_subset = traj[traj['timestamp'] <= two_days]\n",
- "stops_subset = stops[stops['end_timestamp'] <= two_days]\n",
- "traj_clean = clip_spatial_outliers(traj_subset, latitude='latitude', longitude='longitude')\n",
- "\n",
- "plot_pings(traj_clean, ax=ax_map, color='black', s=1.5, alpha=0.3, \n",
- " base_geometry=city, latitude='latitude', longitude='longitude')\n",
- "plot_stops(stops_subset, ax=ax_map, cmap='Reds', base_geometry=city,\n",
- " latitude='latitude', longitude='longitude', radius=stops_subset[\"diameter\"]/2)\n",
- "plot_time_barcode(traj_subset['timestamp'], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops_subset, ax=ax_barcode, cmap='Reds', set_xlim=False, timestamp='timestamp')\n",
- "plt.tight_layout()\n",
- "plt.show()"
+ "print(\"Runtime Disaggregation\")\n",
+ "print(f\"Lachesis clustering time: {execution_time_lachesis} seconds\")\n",
+ "print(\"--------------------------------\")\n",
+ "print(f\"TA-DBSCAN clustering time: {clustering_time_tadbscan} seconds\")\n",
+ "print(f\"TA-DBSCAN post-processing time: {post_time_tadbscan} seconds\")\n",
+ "print(\"--------------------------------\")\n",
+ "print(f\"Grid-Based clustering time: {execution_time_grid} seconds\")\n",
+ "print(\"--------------------------------\")\n",
+ "print(f\"HDBSCAN clustering time: {clustering_time_hdbscan} seconds\")\n",
+ "print(f\"HDBSCAN post-processing time: {post_time_hdbscan} seconds\")"
]
},
{
"cell_type": "markdown",
- "id": "235e25d5",
+ "id": "5c9ee070",
"metadata": {},
"source": [
- "## Runtime Scalability\n",
- "\n",
- "We measure how runtime scales with dataset size by running each algorithm on progressively larger time windows (6-hour increments)."
+ "## Pings vs Runtime"
]
},
{
"cell_type": "code",
- "execution_count": 4,
- "id": "947ce6c3",
+ "execution_count": 5,
+ "id": "62ed6a42",
"metadata": {
"execution": {
- "iopub.execute_input": "2025-10-17T05:59:59.280183Z",
- "iopub.status.busy": "2025-10-17T05:59:59.279974Z",
- "iopub.status.idle": "2025-10-17T06:09:24.164218Z",
- "shell.execute_reply": "2025-10-17T06:09:24.162167Z"
+ "iopub.execute_input": "2025-11-24T18:33:23.492088Z",
+ "iopub.status.busy": "2025-11-24T18:33:23.492088Z",
+ "iopub.status.idle": "2025-11-24T18:33:23.520075Z",
+ "shell.execute_reply": "2025-11-24T18:33:23.520075Z"
}
},
"outputs": [],
"source": [
- "runtime_data = []\n",
- "for current_end in pd.date_range(start=traj['datetime'].min() + pd.Timedelta(hours=6),\n",
- " end=traj['datetime'].max(),\n",
- " freq='6h'):\n",
- " window = traj[traj['datetime'] <= current_end]\n",
- " n_pings = len(window)\n",
- " \n",
- " for name, func in algorithms:\n",
- " t0 = time.time()\n",
- " func(window)\n",
- " runtime_data.append({'Algorithm': name, 'n_pings': n_pings, 'runtime': time.time() - t0})\n",
+ "traj = loader.sample_from_file(filepath_root, frac_users=0.1, format='parquet', traj_cols=tc, seed=10)\n",
"\n",
- "runtime_df = pd.DataFrame(runtime_data)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "07203969",
- "metadata": {},
- "source": [
- "## Performance Comparison"
+ "# H3 cells for grid_based stop detection method\n",
+ "traj['h3_cell'] = filters.to_tessellation(traj, index=\"h3\", res=10, traj_cols=tc, data_crs='EPSG:3857')\n",
+ "pings_per_user = traj['gc_identifier'].value_counts()"
]
},
{
"cell_type": "code",
- "execution_count": 5,
- "id": "570b6103",
+ "execution_count": 6,
+ "id": "baafc0b8",
"metadata": {
"execution": {
- "iopub.execute_input": "2025-10-17T06:09:24.247662Z",
- "iopub.status.busy": "2025-10-17T06:09:24.247376Z",
- "iopub.status.idle": "2025-10-17T06:09:24.624693Z",
- "shell.execute_reply": "2025-10-17T06:09:24.623659Z"
+ "iopub.execute_input": "2025-11-24T18:33:23.520075Z",
+ "iopub.status.busy": "2025-11-24T18:33:23.520075Z",
+ "iopub.status.idle": "2025-11-24T18:33:24.093854Z",
+ "shell.execute_reply": "2025-11-24T18:33:24.093854Z"
}
},
"outputs": [
{
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAABW0AAAHqCAYAAAB/bWzAAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Qd4U9X7B/A3nXS3QAd7I7IRxA0oCKiAGzco+OMvTlwsF6CyRAT3QnCggltQEUQ2yBJFQZS9aaF07yb5P9/T3jRJ0zZtk+Ym/X6eJ/bm3pubk3tLPXnve95jMJvNZiEiIiIiIiIiIiIiXfDzdAOIiIiIiIiIiIiIqASDtkREREREREREREQ6wqAtERERERERERERkY4waEtERERERERERESkIwzaEhEREREREREREekIg7ZEREREREREREREOsKgLREREREREREREZGOMGhLREREREREREREpCMM2hIRERERERERERHpCIO2RFTrrV69WgwGg/qpufvuu6V58+birRYsWKA+06FDh0RPjh49KnXq1JENGza45fiTJk1Sn9sariOuJ5WvoKBAmjRpIm+++aanm0JEREQ6gD4V+lbk/fT63YCIysegLRFV265du+TOO++URo0aSXBwsDRs2FDuuOMOtd7X9enTR3WAtEdISIh07txZ5syZIyaTye3vP3XqVPn222/FW0yZMkUuuOACueSSSyzrEFC1PofWj2XLltVY26zfNyAgQOrWrSvdu3eXRx55RHbv3l3l42ZnZ6svPNY3BTxp48aNqj2pqak26wMDA+Wxxx6TF198UXJzcz3WPiIiotoYTLPug6BPjf7R8ePH3f7+P/74IwOzDvr1fn5+EhkZKeecc47cddddsmLFimodGzfFca1dLT8/X+bOnSvdunVT7Y2OjpYOHTrIqFGjZM+ePS5/PyKqWQE1/H5E5GO+/vprue2221SAa+TIkdKiRQt1B3fevHny5Zdfyueffy7XX3+9+LLGjRvLtGnT1PKZM2fk008/lUcffVROnz6tAmDuDtredNNNct1119msR+fy1ltvVUF0vcD5+PDDD9XDHtr5/vvvl1rfpUsXqUlXXnmlDBs2TMxms6Slpcmff/6p2ouO9owZM1RQsypB28mTJ1u+DOghaIv24MsgOvbW7rnnHhk/frz6HR4xYoTH2khERFTb4MY2+tG4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BGzoT06ZMkX13S6++GL566+/VDkCbZRbedBPRZkI9P9QRgt9TPT9cA4weS0C/mWVrcBIr927d6t6tt99953NNpw33LxHfWB8LmR9Y9JbfF70GxEsRv8YGc/onxKRG5mJiKpp586d5ttuu83coEEDc2BgoDkhIUE9/+uvv8p8zYoVK3BL2WwwGMxHjx51uM/+/fvNw4YNU8fDcRs1amQeNGiQ+csvv7TsM3/+fHWcrVu3lnp9SkqK+Z577jHXr1/fHB4ebh4wYIB5z5495mbNmpmHDx9u2W/VqlXqGPipwXbsV5HevXubO3To4HDb6tWr1XGfe+459Tw7O9s8cuRIc1RUlDkiIsI8dOhQc1JSks0+gGWsO336tM3xtM968OBByzp8nl69eplDQkLUNu1zOdoXn+eaa64p1U7s98ADD9isw+uw/qWXXqr0NSlLYmKiOSAgwPzxxx/brEebw8LCyn0tzsWNN95oDg0NNcfExJj/7//+z/z333+rNuKz2p87a/bXuyx4nfbw8/MzR0dHm7t162Z+5JFHzLt27Sq1v8lkMk+dOlUdPzg4WO27dOlSh787GzduNHfv3t0cFBRkc72PHTtmvv7669V74ffi5ptvNp84ccJmn7y8PPOTTz5p7tKli/q9wbnC8ptvvlmqTTt27DDfcMMN5nr16qk2oR34PVu5cqXNfs8//7y6dvic1r8nqampqo3vv/9+heeLiIiIqq+8vqzRaDS3atVKPQoLC9W6Tz75xNyyZUv1/+uuXbuaf/7551J9j7L6cWDf78RxH3roIXNsbKzql1v3o5zto5bVl3PUT87PzzfPmDFDrUdfBf069JEmT55sTktLc+qcPfXUU6odrVu3LrXt999/V99DmjZtqo4fFxen+qrbtm2r8Lhor3V/EN8f2rRpY77zzjvNy5cvd/ia77//3ty5c2dznTp1zM2bN1ef7YMPPijVDz916pTqh6Mvh214L8jNzTU//vjj6nsU+vOXXHKJedOmTWq7tk95fevp06er/fB69LNxPq+44opSfXP77wa4Ztaf1fph329+99131TVC+9D+Tp06mceOHav6rETkXgb8x51BYSIiIg2yQpDNinrHpD/IyEDmL7JJWLuMiIiIiIjIcxi0JSKiGoO6WygRgAm4MCSO9AMlGjAcDpOdYLIzIiIiIiIi8hwGbYmIiIiIiIiIiIh0xM/TDSAiIiIiIiIiIiKiEgzaEhEREREREREREekIg7ZEREREREREREREOsKgLREREREREREREZGOBHi6AVTCZDLJiRMnJCIiQgwGg6ebQ0REROQVMK9uRkaGNGzYUPz8mJNQU9h3JSIiInJf35VBWx1Bp7dJkyaebgYRERGRVzp69Kg0btzY082oNdh3JSIiInJf35VBWx1BlgIcPnxYoqOjPd0cn84KOX36tMTGxjIbx414nmsOz3XN4HmuGTzPNceXznV6eroKHmp9KaoZ2vnGF47IyEif/N3yVbxG+sbro2+8PvrG66NvvD6V67syaKsj2rAydHqtO77k+j8Subm56hzX5j8S7sbzXHN4rmsGz3PN4HmuOb54rjlEXx99V1/83fI1vEb6xuujb7w++sbro2+8PpXru/IMEREREREREREREekIg7ZEREREREREREREOsKgLREREREREREREZGOsKYtERERERG5jdFolIKCAk83g+xqCuKaoK6gu2oKBgYGir+/v1uOTUREVBswaEtERERERC5nNpvl5MmTkpqa6ummkINrg8BtRkaGWyfwi46OloSEBE4SSEREVAUM2hIRERERkcslJiZKWlqaxMXFSWhoKAN3OgvaFhYWSkBAgFuuC46fnZ0tSUlJ6nmDBg1c/h5ERES+jkFbIiIiIiJyKWRxIsM2Pj5e6tWr5+nmUA0HbSEkJET9ROAWgXuWSiAiIqocTkRGREREREQur2MLyLCl2ku7/qxpTEREVHkM2hIRERERkVuwJELtxutPRERUdQzaEhEREREREREREekIg7ZEREREREQ1oE+fPjJmzBi3HX/BggUSHR3ttuMTERFRzeFEZEREREREpEtGk1m2HDwrSRm5EhdRR3q2qCv+fu4dcn/33XerSdS+/fZb8Ta33HKLXH311Z5uBhEReZDZaJTsbdul8PRpCYiNldAe3cXAySC9EoO2RERERESkO8v+PimTl+yWk2m5lnUNourIc4Pby8CODTzaNr0KCQlRDyIiqp3Sly+XxKnTpPDUKcu6gIQEiZ84QSL79/do26jyWB6BiIiIiIh0F7Ad/cnvNgFbOJWWq9ZjuyfMnj1bOnXqJGFhYdKkSRO5//77JTMz02afDRs2qDIIoaGhEhMTIwMGDJCUlBTLdpPJJGPHjpW6detKQkKCTJo0yeb1yPK9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/vvvP3Uce9rxUS+XiIj0heURfAuDtkRERERE5H7OlihArdo5HYsmHXNY19YgEtlQZMxfIn5Fk3fVBGSf9urVS9544w2b9ch6xTYEbRFIdRYCo6+++qoK2Hbr1q3cfZFdqwWDHYmJiZEGDRpYatFiojSUPPjuu+9K1bVFUBYlGMLCwlTm8OOPP16qFML999+vtk2fPr3UezVp0kRNSjZx4kRLyQYiItIHBm19C8sjEBERERGRfiAQO3BG8ZOSsgE2zwdOr9GAbUFBgXz88cdy2223qbqv1o97771XNm/eLLt27Sr3GMnJyXLq1Ck5cOCAfP/992riL2TtIuCLoKx1UBX74XHw4EFVzxY1bLXg6zvvvCOjR49W2bf79+9X74vMX/zUMnGHDh0qt9xyi2ovSids27ZNTXi2dOlS9b6YfAzlF37//XfVfvvPhNdhArTCwkKHn2XChAly4sQJ+eWXX1x6nomIqHqMKSmebgK5EIO2RERERESkL+2HiAz9SCSyge16ZNhiPbbXIARZEXS9/vrrS20799xz1aOsOrAaBEuRDdupUydVExav2blzp1x++eU2+6Wnp6v98MA+L7/8skyZMkWeeuoptR0TlmVmZqo6t6hjiwnJULYB5QqwrNXIRemD2bNnW9ajzMKkSZNU8HfAgAGyYMECVaNWmwzNGj5nUlKS/Pjjjw4/S926dVWgGKUViIhIH3DTz5iW5ulmkAsZzM5WqSe3QwctKipKmo1ZJBIc5unm+Cw/Mcu5MWb5J8UgplLZG+QqPM81h+e6ZvA81wyeZ98514emXyM13YfCkO/IyMgae9/arqzzjmH/x48fV5NztWzZUurUqVP1N0GphMMbRTITRcLjRZpdXKMZtr4KXwGRRRsQEGAzCZqrIaiLbGGUgKjW70Etg39DCJrHxcWpGsekL7w++labr48xPV3+61lSoxxCL7xQmi2YL3pRm69PVfqurGlLRERERET6hABti8s83QoiIiLdYz1b31N7w9pEREREREREREQ+gPVsfQ+DtkRERERERERERF6Mmba+h0FbIiIiIiIiIiIiL8agre9h0JaIiIiIyMdMmzZNzj//fImIiFCTfVx33XXy77//lpok6oEHHpB69epJeHi43HjjjZKYmOixNhMREVHVMWjrexi0JSIiIiK3Wbt2rQwePFgaNmyoZqn/9ttvLdsK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SiWAVhpuw4YNqsatlsmLQKx9VjqOj2QUPFBLF5OboTYunuM7EbJy7eseW7ddK1/iqO1ERKQfLI/gm3SRaUtERERERAQIxI7vOV4eW/2YCtBaB27xHMb1HOfygK0GtY8vueQSVfoASSOdO3dW9ZGRoYoJjbt37+70sTCSD8dCNuy1114rP//8s1P1bAGl3lAaDmUZ9u7dK1OmTFGj+hBMBUwehrIgmHwM2cEYLYh9NciiRY1dzBOCEYg//vij2o6sXWThop4zRjciCQUl49LS0lQgGKMJUXMa5eBQzxYT8mGCZ5SxKysxh4iIPItBW9/k5+kGEBERERERWevXrJ/M7jNb4kLjbNYjwxbrsd1dUIINkxYjGxUTf2GyLwRwX3vtNVWSAPNiOOvCCy+U9957T01IhuOgTMHTTz/t1GsxwRlKw6F8AcoiYN6Mn376yTJ3BibOQzAWE4khcIvJ9s477zzL65FVi6DuFVdcoQK9b7/9tqrTjOMAJtVDW6ZNm6a2o+Yt6uO2aNFCbW/atKl89dVX8u2336q24/VTp06t5NkkIqKaYEyxCtqypq3PMJidrU9AbodJNFBHt9mYRSLB1RuKRWXzE7OcG2OWf1IMYirO1iDX43muOTzXNYPnuWbwPPvOuT40/Rqp6T4UMgUdzTlANXvekc15/PhxVYMVtWAxKVxVoVTC70m/y+ns0xIbGivnxZ3ntgzb2gRfATFxGQLAyNJ1F0w2ffDgQRUIrs7vQW2Df0OYrDsuLk5leZO+8ProW229PsefeFLSly5Vyy2+/UYOXldUOif0wgul2YL5ohe19fpUte/K8ghERERERKRLCNCen3C+p5tBRESkayyP4Jtqb1ibiIiIiIiIiIjIV4K2fn7iHxHh6eaQizBoS0RERERERERE5KWMKSnqp39UlArckm/glSQiIiIiIiIiIvLyTFuWRvAtDNoSERERERERERF5IXN+vpiystQyg7a+hUFbIiIiIiIiIiIiL2RMS7Ms+8fEeLQt5FoM2hIREREREREREXmhwuJ6tsBMW9/CoC0REREREREREZEX17MFBm19C4O2REREREREREREXohBW9/FoC0REREREREREZEXMqZYBW1jGLT1JQzaEhERERGRLpmNRsnavEXSlv6gfuK5u919991y3XXXlVq/evVqMRgMkpqaalnGw8/PT6KioqRbt24yduxYOXnypM3rJk2aZNkXD+x72WWXyZo1a2z2++abb+TCCy9U2yMiIqRDhw4yZswYm33y8/Nl5syZ0qVLFwkNDZX69evLJZdcIvPnz5eCggKbfTdt2iT+/v5yzTXXlPoshw4dkqCgIImPj5eMjAybbV27dlVtJiIi78BMW9/FoC0REREREelO+vLlsq9vPzkyfLiceOIJ9RPPsV4v/v33Xzlx4oRs3bpVxo0bJ7/88ot07NhR/vrrL5v9EIBFMBcPBFPbtGkjgwYNkrTiGb9Xrlwpt9xyi9x4442yZcsW2b59u7z44os2gVgEbAcMGCDTp0+XUaNGycaNG9W+DzzwgLz22muya9cum/ecN2+ePPTQQ7J27VrVRkcQsJ01a5Zbzg0REdV80DaAQVufEuDpBhAREREREVlDYPb4I2NEzGab9YWJiUXr586RyP79xdPi4uIkOjpaEhISpG3btnLttdeqjNvRo0fL+vXrLfsFBASofQA/p0yZorJj//vvPzn//PNlyZIlKmP2ySeftLwGx7PO+J0zZ44KwG7btk29h6Zly5Zy8803q6CuJjMzUxYtWqT2PXXqlCxYsEAmTpxYqv0PPvigzJ49WwV+8VmIiMj7MNPWdzHTloiIiIiIdAMlEBKnTisVsC3aWLQO22uiVEJlhYSEyH333ScbNmyQpKQkh/vk5eWpgC2Cveecc44lkItM2b///rvMYy9cuFD69etnE7DVBAYGSlhYmOX54sWLpV27dur4d955p3zwwQdidnA+b7vtNmndurUKIhMRkXcypqRYlv1jYjzaFnItZtoSEREREZHbHbzxJik8c6bC/Uz5+WKy+gJaitkshadOyX+XXiZ+QUEVHi+gfn1p8dWXlWrr0qVLJTw83Gad0ckgMYKlWt1YLXsV5RK042VnZ6uatciEjYyMVOtQxmDdunXSqVMnadasmapt279/f7njjjskODhY7bN3717p06ePU21AaQQEa2HgwIGqDANq6Nq/HjV2UW5h8ODB8uijj0qrVq2cOj4REek00zYqSsyFhR5tD7kOg7ZEREREROR2CNiivIGrILBrEve4/PLL5a233rJZt3nzZksgtDxaRisCohpkvH7//feWOrII2KKkwapVq6RHjx4qS/aHH36Q/fv3q3W//fabPP744zJ37lxVAxeTjjnKlC2rzi5q3WJiM600A+rlIpDrKOiLOrmXXnqpPPPMM/Lpp5869R5ERKS/oK1feLgYAgMZtPUhDNoSEREREZHbIePVGRVm2hbzi4lxOtO2shBERdkAa8eOHXPqtf/884/62bx5c8u6oKAgm+OhxMG3336r6tR+8sknlvXIdMXj3nvvlaeeekrVtUWA95577lHLe/bsqfD9EZwtLCyUhg0bWtYh4IuM3ddff12ioqJKvQbZthdddJFNTV0iIvKuoC1LI/geBm2JiIiIiMjtnC1RgFq1+/r2K8rKdZRdajBIQHy8tF75ixj8/UVPcnJy5N1335VevXpJbGxsufv6+/ur/cuCoC8ybLOystTz22+/XU0mtmPHjlJ1bQsKCtREZAjMfvTRR/Lyyy+r8grWMKnZZ599pmru2uvZs6fccMMNMn78+Ep+YiIi8iT8P9OYlqaWOQmZ72HQloiIiIiIdAOB2PiJE+T4I2NUgNYmcFtccgDb9RCwxWRjubm5quTB9u3bZebMmXLmzBn5+uuvbfZD5uupU6dsyiPs3r1bxo0bp9ZNmjRJ1bq9+uqrVU3b1NRUefXVV1Uw9sorr1T7jBkzRpVQ6Nu3rzz//POqpAFq427btk1mzJihMmxRRzclJUVGjhxZKqP2xhtvVPs4CtrCiy++KB06dFDlFIiIyDsY09Mt/59k0Nb3+Hm6AURERERERNYi+/eXRnPnqIxaa3iO9diuB6hVizIE3bt3VyUG+vXrJ3///be0b9/eZr9du3ZJgwYN1KNr166yePFiVTN32LBhanvv3r3lwIED6jkmMrvqqqtUkHf58uXqPQBZtCtWrJCxY8fKO++8oyYrO//881Vw9+GHH5aOHTuqoCza4KgEAoK2CPDu3LnT4WdB+YURI0aoIDQREXnhJGQM2voc3kYlIiIiIiLdQWA2om9fyd62XQpPn5aA2FgJ7dHd7Rm2CxYscLgek3hpk4FZL1cEWbR4VDTxGR4VQeAWJQzKKmOwZMmSMl+LEgham/ET5RTss2oRDMaDiIi8gzHFKmgbw6Ctr2HQloiIiIiIdAkB2rALenq6GURERLrETFvfxvIIREREREREREREXoZBW9/GoC0REREREREREZEXB20DGLT1OQzaEhEREREREREReRljSopl2T8mxqNtIddj0JaIiIiIiNzC2cm6yDfx+hMRuRfLI/g2Bm2JiIiIiMil/P391c/s7GxPN4U8SLv+gYGBnm4KEZFPYtDWtwV4ugFERERERORb/Pz8JDo6WpKSktTz0NBQMRgMnm4WWWXAFhYWSkBAgFuuC46PgC2uP34PtCA+ERG5FoO2vo1BWyIiIiIicrn4+HgVENQCt6QfCKqaTCYVXHdnMB0B24SEBLcdn4iotjOmFtW0NdSpI34hIZ5uDrkYg7Y69Mdz/VUHh9wDHVR8eYiLi1MdVXIPnueaw3NdM3ieawbPc83huSZ3QzCwQYMG6nesoKDA080hu3//ycnJUq9ePbf9+0dJBGbYEhG5V2Fxpi2zbH0Tg7ZEREREROQ2CNwxeKe/oC2CqnWQmcWbNkREXjtqwpiappYZtPVN/D80ERERERERERGRFzFlZYkUj2Txj2HQ1hcxaEtERERERERERORFOAmZ72PQloiIiIiIiIiIyIsYUxi09XUM2hIREREREREREXkRZtr6PgZtiYiIiIiIiIiIvIgxNcWyHBAT49G2kHswaEtERERERERERORFWB7B9zFoS0RERERERERE5EVYHsH3MWhLRERERERERETkrUFblkfwSQzaEhEREREREREReWlNW2ba+iYGbYmIiIiIiIiIiLwIyyP4PgZtiYiIiIiIiIiIvEihFrQNCBC/8HBPN4fcgEFbIiIiIiIiIiIiL2JMSbVk2RoMBk83h9yAQVsiIiIiIiIiIiIvLI/gHx3l6aaQmzBoS0RERERERERE5CVMeXlizslRy6xn67sYtCUiIiIiIiIiIvISnISsdmDQloiIiIiIiIiIyEsYU1IsywExMR5tC7kPg7ZERERERERERERegpm2tUOApxtApXWdvFwkOKzM7YemX1Oj7SEiIiIiIiIiIn1g0LZ2YKYtERERERERERGRVwZtWR7BVzFoS0RERERERERE5IU1bf1jmGnrqxi0JSIiIiIiIiIi8hIsj1A7MGhLRERERERERETkJRi0rR0YtCUiIiIiIiIiIvIShTblEVjT1lcxaEtEREREREREROQljKlpRQsGg/hHRnq6OeQmDNoSERERERERERF5WXkEv8hIMfj7e7o55CYM2hIREREREREREXlZ0NY/OsrTTSE3YtCWiIiIiIiIiIjIC5gLC8WUnq6WA6JZz9aXMWhLRERERERERETkBYxpxfVsVaZttEfbQu7FoC0REREREREREZEXlUYA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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\r",
+ " 0%| | 0/4 [00:00, ?it/s]"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\r",
+ " 25%|████████████▊ | 1/4 [00:00<00:00, 4.68it/s]"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\r",
+ " 50%|█████████████████████████▌ | 2/4 [00:00<00:00, 5.53it/s]"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\r",
+ " 75%|██████████████████████████████████████▎ | 3/4 [00:00<00:00, 6.61it/s]"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\r",
+ "100%|███████████████████████████████████████████████████| 4/4 [00:00<00:00, 7.10it/s]"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
}
],
"source": [
- "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n",
+ "# Approximately 5 minutes for 40 users\n",
+ "results = []\n",
+ "for user, n_pings in tqdm(pings_per_user.items(), total=len(pings_per_user)):\n",
+ " user_data = traj.query(\"gc_identifier == @user\")\n",
+ "\n",
+ " # For location based\n",
+ " start_time = time.time()\n",
+ " stops_gb = GRID_BASED.grid_based(user_data, time_thresh=240, complete_output=True, traj_cols=tc, location_id='h3_cell')\n",
+ " execution_time = time.time() - start_time\n",
+ " results += [pd.Series({'user':user, 'algo':'grid_based', 'execution_time':execution_time, 'n_pings':n_pings})]\n",
+ " \n",
+ " # For Lachesis\n",
+ " start_time = time.time()\n",
+ " stops_lac = LACHESIS.lachesis(user_data, delta_roam=30, dt_max=240, complete_output=True, traj_cols=tc)\n",
+ " execution_time = time.time() - start_time\n",
+ " results += [pd.Series({'user':user, 'algo':'lachesis', 'execution_time':execution_time, 'n_pings':n_pings})]\n",
"\n",
- "bars = ax1.barh(results_df['Algorithm'], results_df['Runtime (s)'])\n",
- "ax1.set_xlabel('Execution Time (seconds)')\n",
- "ax1.set_title('Overall Runtime (Full Dataset)')\n",
- "ax1.grid(axis='x', alpha=0.3)\n",
- "for bar, val in zip(bars, results_df['Runtime (s)']):\n",
- " ax1.text(val + 0.01, bar.get_y() + bar.get_height()/2, f'{val:.3f}s', va='center')\n",
+ " # For TADbscan\n",
+ " start_time = time.time()\n",
+ " user_data_tadb = user_data.assign(cluster=DBSCAN.ta_dbscan_labels(user_data, time_thresh=240, dist_thresh=15, min_pts=3, traj_cols=tc))\n",
+ " # - post-processing\n",
+ " stops_tadb = post.remove_overlaps(user_data_tadb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)\n",
+ " execution_time = time.time() - start_time\n",
+ " results += [pd.Series({'user':user, 'algo':'tadbscan', 'execution_time':execution_time, 'n_pings':n_pings})]\n",
"\n",
- "for algo in runtime_df['Algorithm'].unique():\n",
- " subset = runtime_df[runtime_df['Algorithm'] == algo]\n",
- " ax2.plot(subset['n_pings'], subset['runtime'], marker='o', label=algo, linewidth=2)\n",
+ " # For HDBSCAN\n",
+ " start_time = time.time()\n",
+ " user_data_hdb = user_data.assign(cluster=HDBSCAN.hdbscan_labels(user_data, time_thresh=240, min_pts=3, min_cluster_size=2, traj_cols=tc))\n",
+ " # - post-processing\n",
+ " stops_hdb = post.remove_overlaps(user_data_hdb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3) \n",
+ " execution_time = time.time() - start_time\n",
+ " results += [pd.Series({'user':user, 'algo':'hdbscan', 'execution_time':execution_time, 'n_pings':n_pings})]\n",
+ "\n",
+ "results = pd.DataFrame(results)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "8e8546e8",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:24.093854Z",
+ "iopub.status.busy": "2025-11-24T18:33:24.093854Z",
+ "iopub.status.idle": "2025-11-24T18:33:24.338240Z",
+ "shell.execute_reply": "2025-11-24T18:33:24.338240Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import seaborn as sns\n",
"\n",
- "ax2.set_xlabel('Number of Pings')\n",
- "ax2.set_ylabel('Runtime (seconds)')\n",
- "ax2.set_title('Runtime vs Data Size')\n",
- "ax2.legend()\n",
- "ax2.grid(alpha=0.3)\n",
+ "algos = ['grid_based', 'lachesis', 'tadbscan', 'hdbscan']\n",
+ "palette = dict(zip(algos, sns.color_palette(n_colors=len(algos))))\n",
"\n",
- "plt.tight_layout()\n",
+ "fig, ax = plt.subplots(figsize=(5, 5))\n",
+ "sns.scatterplot(data=results, x='n_pings', y='execution_time', hue='algo', ax=ax)\n",
+ "ax.set_title('n_pings vs execution_time')\n",
"plt.show()"
]
}
@@ -268,9 +411,9 @@
"formats": "ipynb,py:percent"
},
"kernelspec": {
- "display_name": "Python 313 (nomad-venv)",
+ "display_name": "Python 3 (ipykernel)",
"language": "python",
- "name": "nomad-venv"
+ "name": "python3"
},
"language_info": {
"codemirror_mode": {
@@ -282,7 +425,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.13.6"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/docs/source/benchmarking_of_stop_detection_algorithms.py b/docs/source/benchmarking_of_stop_detection_algorithms.py
index 78db2b69..3c9f5d7b 100644
--- a/docs/source/benchmarking_of_stop_detection_algorithms.py
+++ b/docs/source/benchmarking_of_stop_detection_algorithms.py
@@ -8,135 +8,188 @@
# format_version: '1.3'
# jupytext_version: 1.17.3
# kernelspec:
-# display_name: Python 313 (nomad-venv)
+# display_name: Python 3 (ipykernel)
# language: python
-# name: nomad-venv
+# name: python3
# ---
# %% [markdown]
-# # Benchmarking Stop Detection Algorithms
-#
-# This notebook compares the performance of four stop detection algorithms: **Lachesis**, **TA-DBSCAN**, **Grid-Based**, and **HDBSCAN**. We evaluate both overall runtime on a full trajectory dataset and how runtime scales with increasing data size.
+# # Comparing runtimes of different stop detection algorithms on toy datasets
# %% [markdown]
-# ## Setup
+# Here we compare the runtimes of four different stop detection algorithms: Lachesis, grid-based, temporal DBSCAN, and HDBSCAN.
# %%
-import time
-import warnings
-import pandas as pd
+# %matplotlib inline
+
+# Imports
+import nomad.io.base as loader
import geopandas as gpd
+from shapely.geometry import box
+import pandas as pd
+import numpy as np
import matplotlib.pyplot as plt
-import nomad.io.base as loader
-import nomad.filters as filters
-from nomad.stop_detection.viz import plot_pings, plot_stops, plot_time_barcode, plot_stops_barcode, clip_spatial_outliers
-import nomad.stop_detection.lachesis as LACHESIS
+from nomad.stop_detection.viz import plot_stops_barcode, plot_pings, plot_stops, plot_time_barcode
import nomad.stop_detection.dbscan as DBSCAN
+import nomad.stop_detection.lachesis as LACHESIS
import nomad.stop_detection.grid_based as GRID_BASED
import nomad.stop_detection.hdbscan as HDBSCAN
+import nomad.filters as filters
+import nomad.stop_detection.postprocessing as post
+import time
+from tqdm import tqdm
+
+# Load data
+import nomad.data as data_folder
+from pathlib import Path
+data_dir = Path(data_folder.__file__).parent
+city = gpd.read_file(data_dir / 'garden-city-buildings.geojson')
+outer_box = box(*city.total_bounds).buffer(15, join_style='mitre')
+
+filepath_root = 'gc_data_long/'
+tc = {
+ "user_id": "gc_identifier",
+ "timestamp": "unix_ts",
+ "x": "dev_x",
+ "y": "dev_y",
+ "ha":"ha",
+ "date":"date"}
+
+users = ['admiring_brattain']
+traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','==', '2024-01-01'), traj_cols=tc)
+
+# Lachesis (sequential stop detection)
+start_time = time.time()
+stops = LACHESIS.lachesis(traj, delta_roam=20, dt_max = 60, dur_min=5, complete_output=True, keep_col_names=True, traj_cols=tc)
+execution_time_lachesis = time.time() - start_time
+print(f"Lachesis execution time: {execution_time_lachesis} seconds")
+
+# Density based stop detection (Temporal DBSCAN)
+start_time = time.time()
+user_data_tadb = traj.assign(cluster=DBSCAN.ta_dbscan_labels(traj, time_thresh=240, dist_thresh=15, min_pts=3, traj_cols=tc))
+clustering_time_tadbscan = time.time() - start_time
+start_time_post = time.time()
+cluster_labels_tadb = post.remove_overlaps(user_data_tadb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)
+execution_time_tadbscan = time.time() - start_time
+post_time_tadbscan = time.time() - start_time_post
+print(f"TA-DBSCAN execution time: {execution_time_tadbscan} seconds")
+print(f"TA-DBSCAN clustering time: {clustering_time_tadbscan} seconds")
+print(f"TA-DBSCAN post-processing time: {post_time_tadbscan} seconds")
+
+# Grid-based
+start_time = time.time()
+traj['h3_cell'] = filters.to_tessellation(traj, index="h3", res=10, x='dev_x', y='dev_y', data_crs='EPSG:3857')
+stops_gb = GRID_BASED.grid_based(traj, time_thresh=240, complete_output=True, timestamp='unix_ts', location_id='h3_cell')
+execution_time_grid = time.time() - start_time
+print(f"Grid-Based execution time: {execution_time_grid} seconds")
+
+# HDBSCAN
+start_time = time.time()
+user_data_hdb = traj.assign(cluster=HDBSCAN.hdbscan_labels(traj, time_thresh=240, min_pts=3, min_cluster_size=2, traj_cols=tc))
+clustering_time_hdbscan = time.time() - start_time
+start_time_post = time.time()
+cluster_labels_hdb = post.remove_overlaps(user_data_hdb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)
+execution_time_hdbscan = time.time() - start_time
+post_time_hdbscan = time.time() - start_time_post
+print(f"HDBSCAN execution time: {execution_time_hdbscan} seconds")
+print(f"HDBSCAN clustering time: {clustering_time_hdbscan} seconds")
+print(f"HDBSCAN post-processing time: {post_time_hdbscan} seconds")
-city = gpd.read_file("../../examples/garden_city.geojson")
-traj = loader.sample_from_file('../../examples/gc_data/', users=['youthful_mayer'], format='csv')
+# %% [markdown]
+# ## Summary of Single-User Performance
# %% [markdown]
-# ## Overall Runtime Comparison
-#
-# We first measure the total execution time for each algorithm on the complete dataset.
+# ### Lachesis
# %%
-algorithms = [
- ('Lachesis', lambda t: LACHESIS.lachesis(t, delta_roam=20, dt_max=60, dur_min=5,
- complete_output=True, keep_col_names=True,
- latitude="latitude", longitude="longitude")),
- ('TA-DBSCAN', lambda t: DBSCAN.ta_dbscan(t, time_thresh=240, dist_thresh=15, min_pts=3,
- dur_min=5, complete_output=True,
- latitude="latitude", longitude="longitude")),
- ('Grid-Based', lambda t: GRID_BASED.grid_based(
- t.assign(h3_cell=filters.to_tessellation(t, index="h3", res=10,
- latitude='latitude', longitude='longitude',
- data_crs='EPSG:4326')),
- time_thresh=240, complete_output=True, timestamp='timestamp', location_id='h3_cell')),
- ('HDBSCAN', lambda t: HDBSCAN.st_hdbscan(t, time_thresh=240, min_pts=3, min_cluster_size=2,
- dur_min=5, complete_output=True,
- latitude="latitude", longitude="longitude"))
-]
+fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),
+ gridspec_kw={'height_ratios':[10,1]})
-results = []
-for name, func in algorithms:
- t0 = time.time()
- stops_output = func(traj)
- results.append({'Algorithm': name, 'Runtime (s)': time.time() - t0})
- if name == 'Lachesis':
- stops = stops_output
+gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')
+city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')
-results_df = pd.DataFrame(results)
-print(results_df.to_string(index=False))
+plot_stops(stops, ax=ax_map, cmap='Reds', x='x', y='y')
+plot_pings(traj, ax=ax_map, s=6, point_color='black', cmap='twilight', traj_cols=tc)
+ax_map.set_axis_off()
-# %% [markdown]
-# ## Visualization
-#
-# Spatial and temporal visualization of detected stops using the Lachesis algorithm.
+plot_time_barcode(traj[tc['timestamp']], ax=ax_barcode, set_xlim=True)
+plot_stops_barcode(stops, ax=ax_barcode, cmap='Reds', set_xlim=False, timestamp='unix_ts')
-# %%
-fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6, 6.5),
- gridspec_kw={'height_ratios': [10, 1]})
-
-two_days = 1704162819 + 3600*48
-traj_subset = traj[traj['timestamp'] <= two_days]
-stops_subset = stops[stops['end_timestamp'] <= two_days]
-traj_clean = clip_spatial_outliers(traj_subset, latitude='latitude', longitude='longitude')
-
-plot_pings(traj_clean, ax=ax_map, color='black', s=1.5, alpha=0.3,
- base_geometry=city, latitude='latitude', longitude='longitude')
-plot_stops(stops_subset, ax=ax_map, cmap='Reds', base_geometry=city,
- latitude='latitude', longitude='longitude', radius=stops_subset["diameter"]/2)
-plot_time_barcode(traj_subset['timestamp'], ax=ax_barcode, set_xlim=True)
-plot_stops_barcode(stops_subset, ax=ax_barcode, cmap='Reds', set_xlim=False, timestamp='timestamp')
-plt.tight_layout()
+plt.tight_layout(pad=0.1)
plt.show()
+# %%
+print("Summary of Single-User Performance")
+print(f"Lachesis execution time: {execution_time_lachesis} seconds")
+print(f"TA-DBSCAN execution time: {execution_time_tadbscan} seconds")
+print(f"Grid-Based execution time: {execution_time_grid} seconds")
+print(f"HDBSCAN execution time: {execution_time_hdbscan} seconds")
+
+# %%
+print("Runtime Disaggregation")
+print(f"Lachesis clustering time: {execution_time_lachesis} seconds")
+print("--------------------------------")
+print(f"TA-DBSCAN clustering time: {clustering_time_tadbscan} seconds")
+print(f"TA-DBSCAN post-processing time: {post_time_tadbscan} seconds")
+print("--------------------------------")
+print(f"Grid-Based clustering time: {execution_time_grid} seconds")
+print("--------------------------------")
+print(f"HDBSCAN clustering time: {clustering_time_hdbscan} seconds")
+print(f"HDBSCAN post-processing time: {post_time_hdbscan} seconds")
+
# %% [markdown]
-# ## Runtime Scalability
-#
-# We measure how runtime scales with dataset size by running each algorithm on progressively larger time windows (6-hour increments).
+# ## Pings vs Runtime
# %%
-runtime_data = []
-for current_end in pd.date_range(start=traj['datetime'].min() + pd.Timedelta(hours=6),
- end=traj['datetime'].max(),
- freq='6h'):
- window = traj[traj['datetime'] <= current_end]
- n_pings = len(window)
-
- for name, func in algorithms:
- t0 = time.time()
- func(window)
- runtime_data.append({'Algorithm': name, 'n_pings': n_pings, 'runtime': time.time() - t0})
+traj = loader.sample_from_file(filepath_root, frac_users=0.1, format='parquet', traj_cols=tc, seed=10) # try frac_users = 0.1
-runtime_df = pd.DataFrame(runtime_data)
+# H3 cells for grid_based stop detection method
+traj['h3_cell'] = filters.to_tessellation(traj, index="h3", res=10, x='dev_x', y='dev_y', data_crs='EPSG:3857')
+pings_per_user = traj['gc_identifier'].value_counts()
-# %% [markdown]
-# ## Performance Comparison
+# %%
+# Approximately 5 minutes for 40 users
+results = []
+for user, n_pings in tqdm(pings_per_user.items(), total=len(pings_per_user)):
+ user_data = traj.query("gc_identifier == @user")
+
+ # For location based
+ start_time = time.time()
+ stops_gb = GRID_BASED.grid_based(user_data, time_thresh=240, complete_output=True, timestamp='unix_ts', location_id='h3_cell')
+ execution_time = time.time() - start_time
+ results += [pd.Series({'user':user, 'algo':'grid_based', 'execution_time':execution_time, 'n_pings':n_pings})]
+
+ # For Lachesis
+ start_time = time.time()
+ stops_lac = LACHESIS.lachesis(user_data, delta_roam=30, dt_max=240, complete_output=True, traj_cols=tc)
+ execution_time = time.time() - start_time
+ results += [pd.Series({'user':user, 'algo':'lachesis', 'execution_time':execution_time, 'n_pings':n_pings})]
+
+ # For TADbscan
+ start_time = time.time()
+ user_data_tadb = user_data.assign(cluster=DBSCAN.ta_dbscan_labels(user_data, time_thresh=240, dist_thresh=15, min_pts=3, traj_cols=tc))
+ # - post-processing
+ stops_tadb = post.remove_overlaps(user_data_tadb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)
+ execution_time = time.time() - start_time
+ results += [pd.Series({'user':user, 'algo':'tadbscan', 'execution_time':execution_time, 'n_pings':n_pings})]
+
+ # For HDBSCAN
+ start_time = time.time()
+ user_data_hdb = user_data.assign(cluster=HDBSCAN.hdbscan_labels(user_data, time_thresh=240, min_pts=3, min_cluster_size=2, traj_cols=tc))
+ # - post-processing
+ stops_hdb = post.remove_overlaps(user_data_hdb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)
+ execution_time = time.time() - start_time
+ results += [pd.Series({'user':user, 'algo':'hdbscan', 'execution_time':execution_time, 'n_pings':n_pings})]
+
+results = pd.DataFrame(results)
# %%
-fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
-
-bars = ax1.barh(results_df['Algorithm'], results_df['Runtime (s)'])
-ax1.set_xlabel('Execution Time (seconds)')
-ax1.set_title('Overall Runtime (Full Dataset)')
-ax1.grid(axis='x', alpha=0.3)
-for bar, val in zip(bars, results_df['Runtime (s)']):
- ax1.text(val + 0.01, bar.get_y() + bar.get_height()/2, f'{val:.3f}s', va='center')
-
-for algo in runtime_df['Algorithm'].unique():
- subset = runtime_df[runtime_df['Algorithm'] == algo]
- ax2.plot(subset['n_pings'], subset['runtime'], marker='o', label=algo, linewidth=2)
-
-ax2.set_xlabel('Number of Pings')
-ax2.set_ylabel('Runtime (seconds)')
-ax2.set_title('Runtime vs Data Size')
-ax2.legend()
-ax2.grid(alpha=0.3)
-
-plt.tight_layout()
+import seaborn as sns
+
+algos = ['grid_based', 'lachesis', 'tadbscan', 'hdbscan']
+palette = dict(zip(algos, sns.color_palette(n_colors=len(algos))))
+
+fig, ax = plt.subplots(figsize=(5, 5))
+sns.scatterplot(data=results, x='n_pings', y='execution_time', hue='algo', ax=ax)
+ax.set_title('n_pings vs execution_time')
plt.show()
diff --git a/docs/source/grid_based_demo.ipynb b/docs/source/grid_based_demo.ipynb
index 03022d0a..90114911 100644
--- a/docs/source/grid_based_demo.ipynb
+++ b/docs/source/grid_based_demo.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
- "id": "92838936",
+ "id": "f56b531d",
"metadata": {},
"source": [
"# Grid-Based Stop Detection"
@@ -10,7 +10,7 @@
},
{
"cell_type": "markdown",
- "id": "cb276fd9",
+ "id": "9cfdb26e",
"metadata": {},
"source": [
"The stop detection algorithms implemented in `nomad` support different combinations of input formats that are common in commercial datasets, detecting default names when possible\n",
@@ -23,69 +23,78 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "19184dee",
- "metadata": {},
+ "execution_count": 1,
+ "id": "24b50a14",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:11.959806Z",
+ "iopub.status.busy": "2025-11-24T18:32:11.959806Z",
+ "iopub.status.idle": "2025-11-24T18:32:16.169725Z",
+ "shell.execute_reply": "2025-11-24T18:32:16.169725Z"
+ }
+ },
"outputs": [],
"source": [
"%matplotlib inline\n",
+ "import matplotlib\n",
+ "matplotlib.use('TkAgg') # Non-blocking backend\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.ion() # Interactive mode\n",
"\n",
"# Imports\n",
"import nomad.io.base as loader\n",
"import geopandas as gpd\n",
"from shapely.geometry import box\n",
- "import pandas as pd\n",
- "import numpy as np\n",
- "import matplotlib.pyplot as plt\n",
- "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode\n",
+ "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_hexagons, plot_pings\n",
"import nomad.stop_detection.grid_based as GRID_BASED\n",
"import nomad.filters as filters \n",
"\n",
"# Load data\n",
- "city = gpd.read_file(\"garden_city.geojson\").to_crs('EPSG:3857')\n",
- "outer_box = box(*city.total_bounds).buffer(15, join_style='mitre')\n",
+ "import nomad.data as data_folder\n",
+ "from pathlib import Path\n",
+ "data_dir = Path(data_folder.__file__).parent\n",
+ "city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')\n",
+ "outer_box = box(*city.total_bounds)\n",
"\n",
- "filepath_root = '../tutorials/IC2S2-2025/gc_data_long/'\n",
- "tc = {\n",
- " \"user_id\": \"gc_identifier\",\n",
- " \"timestamp\": \"unix_ts\",\n",
- " \"x\": \"dev_x\",\n",
- " \"y\": \"dev_y\",\n",
- " \"ha\":\"ha\",\n",
- " \"date\":\"date\"}\n",
+ "filepath_root = 'gc_data_long/'\n",
+ "tc = {\"user_id\": \"gc_identifier\", \"x\": \"dev_x\", \"y\": \"dev_y\", \"timestamp\": \"unix_ts\"}\n",
"\n",
"users = ['admiring_brattain']\n",
- "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','==', '2024-01-01'), traj_cols=tc)\n",
+ "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)\n",
"\n",
- "# Grid-based\n",
- "traj['h3_cell'] = filters.to_tessellation(traj, index=\"h3\", res=10, x='dev_x', y='dev_y', data_crs='EPSG:3857')\n",
- "stops_gb = GRID_BASED.grid_based(traj, time_thresh=240, complete_output=True, timestamp='unix_ts', location_id='h3_cell')"
+ "# Grid-based - data is in Web Mercator (EPSG:3857) projected coordinates\n",
+ "traj['h3_cell'] = filters.to_tessellation(traj, index=\"h3\", res=10, traj_cols=tc, data_crs='EPSG:3857')\n",
+ "stops_gb = GRID_BASED.grid_based(traj, time_thresh=240, complete_output=True, location_id='h3_cell', traj_cols=tc)"
]
},
{
"cell_type": "code",
- "execution_count": 21,
- "id": "62555a1b",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAACMCAYAAABh9MpJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAf0UlEQVR4nO3deXRU9f3/8dckk8m+sCSWyFYwrCL8vojSooKagobFVhFMwUKpHkQE7TmtonXDys+D39qjglCkgFplEUQPxpZzsA0uHJUlRI9VItqEqoAQCFkJ2T6/P/jd68xkJpkslwzx+ThnDsy9n/v5vO/nfu7n3ndmcxljjAAAAAAAQLuL6OgAAAAAAADorEi6AQAAAABwCEk3AAAAAAAOIekGAAAAAMAhJN0AAAAAADiEpBsAAAAAAIeQdAMAAAAA4BCSbgAAAAAAHELSDQAAAACAQ0i6AQA/GLNnz1bfvn2bLVdUVCSXy6UXXnjB8Zg6St++fTV79uyODgMAgE6PpBsAEPYKCwt11113acCAAYqLi1NcXJyGDBmi+fPn65NPPumwuHbu3CmXy+Xz6Nq1q0aPHq1XXnmlw+I6lz777DM9+uijKioq6uhQAAAIS+6ODgAAgKbk5ORo+vTpcrvdmjFjhoYPH66IiAgdOHBAW7du1cqVK1VYWKg+ffo0W9fq1avV0NDQ7jEuXLhQo0aNkiSdOHFCmzZt0syZM3Xq1CnNnz+/3dsLJ5999pkWL16scePGhfQuAgAAfmhIugEAYeurr77SLbfcoj59+uif//ynevTo4bN+6dKlWrFihSIimn7jVmVlpeLj4xUVFeVInFdeeaWmTp1qP583b5769eun9evXd/qkGwAANI23lwMAwtaTTz6pyspKrVu3rlHCLUlut1sLFy5Ur1697GWzZ89WQkKCvvrqK2VlZSkxMVEzZsyw1/m/Gnvq1CnNnj1bycnJSklJ0axZs3Tq1Kk2xe3xeNSlSxe53b5/2163bp2uueYapaWlKTo6WkOGDNHKlSsbbb93715NmDBB3bt3V2xsrH784x9rzpw5PmUaGhr09NNPa+jQoYqJidEFF1yguXPnqqSkxKecMUaPP/64evbsqbi4OF199dX697//HfK+bNy4USNHjlRiYqKSkpI0bNgwPfPMM5KkF154QTfffLMk6eqrr7bfYr9z5057+xUrVmjo0KGKjo5Wenq65s+f36h/x40bp4svvlj79u3TT3/6U3uf//KXvzSKZ9myZRo6dKji4uLUpUsXXXrppVq/fn3I+wMAwLnGK90AgLCVk5Ojiy66SJdffnmLtqurq9OECRN0xRVX6E9/+pPi4uICljPG6IYbbtD777+vO+64Q4MHD9brr7+uWbNmtai98vJyFRcXS5JOnjyp9evX69NPP9WaNWt8yq1cuVJDhw7VlClT5Ha79eabb+rOO+9UQ0OD/Yr4sWPHNH78eKWmpmrRokVKSUlRUVGRtm7d6lPX3Llz9cILL+jXv/61Fi5cqMLCQi1fvlz79+/Xrl277Ff1H374YT3++OPKyspSVlaW8vLyNH78eNXU1DS7Xzt27FB2drauvfZaLV26VJL0+eefa9euXbr77rt11VVXaeHChXr22Wf1wAMPaPDgwZJk//voo49q8eLFyszM1Lx581RQUKCVK1dqz549PjFKUklJibKysjRt2jRlZ2fr1Vdf1bx58+TxeOw/OKxevVoLFy7U1KlTdffdd6u6ulqffPKJPvroI/3yl79s0TEDAOCcMQAAhKHS0lIjyfz85z9vtK6kpMQcP37cflRVVdnrZs2aZSSZRYsWNdpu1qxZpk+fPvbzN954w0gyTz75pL2srq7OXHnllUaSWbduXZMx5ubmGkmNHhEREWbJkiWNynvHaZkwYYLp16+f/fz11183ksyePXuCtvvee+8ZSeaVV17xWb59+3af5ceOHTMej8dMnDjRNDQ02OUeeOABI8nMmjWryf27++67TVJSkqmrqwtaZvPmzUaSyc3N9VlutT1+/HhTX19vL1++fLmRZNauXWsvGzt2rJFknnrqKXvZmTNnzIgRI0xaWpqpqakxxhhzww03mKFDhzYZMwAA4Ya3lwMAwlJZWZkkKSEhodG6cePGKTU11X4899xzjcrMmzev2Tb+/ve/y+12+5SNjIzUggULWhTrww8/rB07dmjHjh3atGmTsrOz9Yc//MF+G7YlNjbW/n9paamKi4s1duxY/ec//1FpaakkKSUlRdLZV/lra2sDtrd582YlJyfrZz/7mYqLi+3HyJEjlZCQoNzcXEnS22+/rZqaGi1YsEAul8ve/p577glpv1JSUlRZWakdO3aE2hU2q+177rnH5zP3t99+u5KSkvTWW2/5lHe73Zo7d6793OPxaO7cuTp27Jj27dtnx/PNN99oz549LY4HAICOQtINAAhLiYmJkqSKiopG61atWqUdO3bo5ZdfDrit2+1Wz549m23j0KFD6tGjR6PEfuDAgT7PT58+raNHj/o8vA0bNkyZmZnKzMzUtGnT9PLLL2vSpElatGiRjh8/bpfbtWuXMjMzFR8fr5SUFKWmpuqBBx6QJDvpHjt2rG666SYtXrxY3bt31w033KB169bpzJkzdj0HDx5UaWmp0tLSfP74kJqaqoqKCh07dszeP0nKyMjwiTc1NVVdunRptn/uvPNODRgwQNdff7169uypOXPmaPv27c1u5922f196PB7169fPXm9JT09XfHy8z7IBAwZIkv1zZPfdd58SEhJ02WWXKSMjQ/Pnz9euXbtCigcAgI5C0g0ACEvJycnq0aOHPv3000brLr/8cmVmZmrMmDEBt42Ojm72G81bYtOmTerRo4fPoznXXnutqqurtXv3bklnv4n92muvVXFxsf785z/rrbfe0o4dO/Tb3/5WkuyfMnO5XNqyZYs++OAD3XXXXfr22281Z84cjRw50v4DRENDg9LS0uxX1/0fjz32WLvsd1pamvLz87Vt2zZNmTJFubm5uv7661v8mff2MnjwYBUUFGjjxo264oor9Nprr+mKK67QI4880iHxAAAQCr5IDQAQtiZOnKi//vWv2r17ty677LJ2r9/6KbKKigqfV7sLCgp8yk2YMKHFb7Guq6uT9P0r9W+++abOnDmjbdu2qXfv3nY5663g/kaPHq3Ro0dryZIlWr9+vWbMmKGNGzfqtttuU//+/fX2229rzJgxPm9ZD7R/0tlXxvv162cvP378eKNvOQ/G4/Fo8uTJmjx5shoaGnTnnXdq1apVeuihh3TRRRf5vG09UNsFBQU+bdfU1KiwsFCZmZk+5Q8fPmz/tJvliy++kCSfb5yPj4/X9OnTNX36dNXU1OjGG2/UkiVLdP/99ysmJiakfQIA4FzilW4AQNi69957FRcXpzlz5ui7775rtN4Y06b6s7KyVFdX5/OzXfX19Vq2bJlPuR49ethvH7cezcnJyZEkDR8+XNLZz4r7x1xaWqp169b5bFdSUtJov0aMGCFJ9lvMp02bpvr6ev3xj39s1G5dXZ39k1yZmZmKiorSsmXLfOp8+umnm41fkk6cOOHzPCIiQpdccolPLFaS7P8zYJmZ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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "execution_count": 2,
+ "id": "4fea8a03",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:16.169725Z",
+ "iopub.status.busy": "2025-11-24T18:32:16.169725Z",
+ "iopub.status.idle": "2025-11-24T18:32:16.492839Z",
+ "shell.execute_reply": "2025-11-24T18:32:16.492839Z"
}
- ],
+ },
+ "outputs": [],
"source": [
- "fig, ax_barcode = plt.subplots(figsize=(10,1.5))\n",
+ "fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),\n",
+ " gridspec_kw={'height_ratios':[10,1]})\n",
+ "\n",
+ "gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')\n",
+ "city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')\n",
+ "\n",
+ "plot_hexagons(stops_gb, ax=ax_map, color='cluster', cmap='Greens', location_id='h3_cell', data_crs='EPSG:3857')\n",
+ "plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)\n",
+ "ax_map.set_axis_off()\n",
"\n",
"plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops_gb, ax=ax_barcode, stop_color='green', set_xlim=False, timestamp='unix_ts')\n",
- "plt.title(\"Grid-Based stops\")\n",
- "plt.tight_layout()\n",
+ "plot_stops_barcode(stops_gb, ax=ax_barcode, cmap='Greens', set_xlim=False, timestamp='unix_ts')\n",
+ "\n",
+ "plt.tight_layout(pad=0.1)\n",
"plt.show()"
]
}
@@ -106,7 +115,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.14.0"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/docs/source/hdbscan_demo.ipynb b/docs/source/hdbscan_demo.ipynb
index 4437d09a..b7c6c4e4 100644
--- a/docs/source/hdbscan_demo.ipynb
+++ b/docs/source/hdbscan_demo.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
- "id": "92838936",
+ "id": "4f16fcbe",
"metadata": {},
"source": [
"# HDBSCAN Stop Detection"
@@ -10,7 +10,7 @@
},
{
"cell_type": "markdown",
- "id": "cb276fd9",
+ "id": "f69ed80f",
"metadata": {},
"source": [
"The HDBSCAN algorithm constructs a hierarchy of non-overlapping clusters from different radius values and selects those that maximize stability."
@@ -18,73 +18,79 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "19184dee",
- "metadata": {},
+ "execution_count": 1,
+ "id": "3561532d",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:30.624504Z",
+ "iopub.status.busy": "2025-11-24T18:32:30.624504Z",
+ "iopub.status.idle": "2025-11-24T18:32:33.741073Z",
+ "shell.execute_reply": "2025-11-24T18:32:33.740043Z"
+ }
+ },
"outputs": [],
"source": [
"%matplotlib inline\n",
+ "import matplotlib\n",
+ "matplotlib.use('TkAgg')\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.ion()\n",
"\n",
"# Imports\n",
"import nomad.io.base as loader\n",
"import geopandas as gpd\n",
"from shapely.geometry import box\n",
- "import pandas as pd\n",
- "import numpy as np\n",
- "import matplotlib.pyplot as plt\n",
- "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode\n",
+ "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_stops, plot_pings\n",
"import nomad.stop_detection.hdbscan as HDBSCAN\n",
- "import nomad.stop_detection.postprocessing as post\n",
"\n",
"# Load data\n",
- "city = gpd.read_file(\"garden_city.geojson\").to_crs('EPSG:3857')\n",
- "outer_box = box(*city.total_bounds).buffer(15, join_style='mitre')\n",
+ "import nomad.data as data_folder\n",
+ "from pathlib import Path\n",
+ "data_dir = Path(data_folder.__file__).parent\n",
+ "city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')\n",
+ "outer_box = box(*city.total_bounds)\n",
"\n",
"filepath_root = 'gc_data_long/'\n",
- "tc = {\n",
- " \"user_id\": \"gc_identifier\",\n",
- " \"timestamp\": \"unix_ts\",\n",
- " \"x\": \"dev_x\",\n",
- " \"y\": \"dev_y\",\n",
- " \"ha\":\"ha\",\n",
- " \"date\":\"date\"}\n",
+ "tc = {\"user_id\": \"gc_identifier\", \"x\": \"dev_x\", \"y\": \"dev_y\", \"timestamp\": \"unix_ts\"}\n",
"\n",
"users = ['admiring_brattain']\n",
- "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','==', '2024-01-01'), traj_cols=tc)\n",
+ "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)\n",
"\n",
- "user_data_hdb = traj.assign(cluster=HDBSCAN.hdbscan_labels(traj, time_thresh=240, min_pts=3, min_cluster_size=2, traj_cols=tc))\n",
"stops_hdb = HDBSCAN.st_hdbscan(traj,\n",
" time_thresh=720,\n",
- " dist_thresh=15,\n",
" min_pts=3,\n",
" complete_output=True,\n",
- " traj_cols=tc)\n",
- "stops_hdb[\"cluster\"] = post.remove_overlaps(user_data_hdb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3) "
+ " traj_cols=tc) "
]
},
{
"cell_type": "code",
- "execution_count": 30,
- "id": "fa70719e",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxoAAACuCAYAAACx83usAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAm8klEQVR4nO3deXgV9b3H8c8hZCMbEAgQE8ImBCKLhYKCNMQAslMBQ0B2awsUBaUqyhYoiiC3LsXYp/ey+RhZvKKyKJWYsJXlgkChorgQCCk7GghgzPa7f9BzyuGc5CRhJBDer+fJI+d3fjPzm5nvjPlk5syxGWOMAAAAAMBCVSp6AAAAAAAqH4IGAAAAAMsRNAAAAABYjqABAAAAwHIEDQAAAACWI2gAAAAAsBxBAwAAAIDlCBoAAAAALEfQAAAAAGA5ggZwi1q6dKlsNpv27Nnj9v0+ffqoQYMGTm0NGjSQzWaTzWZTlSpVFBISoubNm2vEiBH69NNP3c7H3t/+ExAQoObNm2vWrFm6fPmyU9/Lly9r3rx5at26tYKDgxUUFKTGjRsrISFBmzdvdpn3kSNHNGHCBDVt2lT+/v6qVq2aYmJiNG3aNP3rX/9yO54BAwbIZrNpwoQJbt/ftGmTY6w7duxweX/UqFEKDAx0O21ZJScna+nSpZbM61bTpUsXdenSxfH6ypUrSkpK0qZNm1z6JiUlyWaz6dy5czdvgDfoxIkTSkpK0v79+yt6KLcN+7HlrgYAoDyqVvQAAFirU6dOWrBggSTp0qVLOnz4sFasWKGHHnpIAwcO1PLly+Xt7e00zaBBgzR58mTHNJs3b9bs2bN14MABvf/++5KkwsJCde/eXQcPHtQzzzyj9u3bS5K++eYbrV27Vlu3blVsbKxjnuvWrVNiYqJq1aqlCRMm6N5775XNZtPBgwe1ePFirV+/Xvv27XMax5kzZ7Ru3TpJUkpKihYsWCA/P79i1/XZZ5/V1q1bb3CLFS85OVm1atXSqFGjfrZlVJTk5GSn11euXNGsWbMkySmA3K5OnDihWbNmqUGDBmrTpk1FD+e28Itf/EI7duxQixYtKnooACoJggZQyVSvXl333Xef43XXrl31+9//XklJSZo1a5amTZumefPmOU1Tp04dl2mOHTumlJQU5ebmys/PT1u2bNH27du1ePFijR492tH3oYce0oQJE1RUVORoy8jIUGJiopo2bar09HSFhIQ43nvwwQf15JNP6oMPPnAZ+9tvv638/Hz17t1b69ev1+rVqzV06FC369mjRw9t2LBBa9euVd++fcu+oe5w/DJpvR9//FH+/v4VPYxyCw4OdjoPAMCN4tYp4A6RlJSkmJgYLVy4ULm5uR77h4SEyGazycvLS5J0/vx5SVK9evXc9q9S5T+nkz/96U+6fPmykpOTnUKGnc1m04ABA1zaFy9erDp16mjZsmXy9/fX4sWLix3fqFGj1KJFCz3//PMqLCz0uD7XO3LkiBITExUeHi5fX1/VqVNH8fHxjlttGjRooC+++EKbN2923Kp17a1qmZmZGjZsmMLCwuTr66vmzZvrv/7rv5wC19GjR2Wz2TR//ny9+OKLql+/vvz8/NSuXTt99tlnTuM5e/asfvvb3yoyMlK+vr6qXbu2OnXqpNTU1GLX4YsvvpDNZtN7773naPv8889ls9kUExPj1Ldfv35q27at4/W1t04dPXpUtWvXliTNmjXLsb7XX8k5ffq0hgwZopCQENWpU0djxozRhQsXPG7rLl266J577tHWrVt13333yd/fX3fddZemT5/usu++//57jR8/XnfddZd8fHzUqFEjTZ06VT/99JNTv/fee08dOnRQSEiIqlWrpkaNGmnMmDGSrt4C9Mtf/lKSNHr0aMf6JCUllThO+y1i+/bt04ABAxQcHKyQkBANGzZMZ8+ederboEED9enTR6tXr9a9994rPz8/xxWhf/7zn+rfv79q1KghPz8/tWnTRsuWLXNZXnZ2tiZPnqxGjRrJ19dXYWFh6tWrl7766itHn7y8PM2ZM0fR0dGOuhg9erTLeNLS0tSlSxeFhobK399f9evX18CBA3XlyhVHn7feekutW7dWYGCggoKCFB0drRdeeMHxvrtbp+y3In777bfq1auXAgMDFRkZqcmTJ7vsk6ysLA0aNEhBQUGqXr26Hn30Ue3evVs2m63S3oIIoGRc0QBucYWFhSooKHBpN8aUeV59+/bVyy+/rD179uiBBx5wmpd9GfZbp5YtW6bExETHbVbt2rWTt7e3Jk6cqBkzZujBBx8sNnR8+umnLldJPNm+fbu+/PJLPfPMMwoNDdXAgQOVkpKijIwMNWzY0KW/l5eX5s6dq/79+2vZsmWOXzJLq1evXiosLNT8+fNVv359nTt3Ttu3b1d2drYk6YMPPtCgQYMUEhLiuM3I19dX0tVQ0LFjR+Xl5emPf/yjGjRooHXr1ukPf/iDvvvuO5fbkhYuXKioqCi99tprKioq0vz589WzZ09t3rxZ999/vyRp+PDh2rt3r1588UU1bdpU2dnZ2rt3ryPguRMTE6N69eopNTVVjzzyiCQpNTVV/v7+OnTokE6cOKHw8HAVFBRo8+bNGjt2rNv51KtXTxs2bFCPHj302GOP6Te/+Y0kOcKH3cCBAzV48GA99thjOnjwoJ5//nlJKjEQ2p06dUqJiYmaMmWKZs+erfXr12vOnDn64YcftHDhQklSbm6u4uLi9N1332nWrFlq1aqVtm7dqrlz52r//v1av369JGnHjh0aPHiwBg8erKSkJPn5+enYsWNKS0uTdPUWoCVLlmj06NGaNm2aevfuLUmKiIjwOE5Jevjhh5WQkKCxY8fqiy++0PTp03Xo0CHt2rXL6bbDvXv36ssvv9S0adPUsGFDBQQE6PDhw+rYsaPCwsL0xhtvKDQ0VO+8845GjRql06dP69lnn5Uk5eTk6IEHHtDRo0f13HPPqUOHDrp06ZK2bNmikydPKjo6WkVFRerfv7+2bt2qZ599Vh07dtSxY8c0c+ZMdenSRXv27JG/v7+OHj2q3r17q3Pnzlq8eLGqV6+uf/3rX9qwYYPy8vJUrVo1rVixQuPHj9cTTzyhBQsWqEqVKvr222916NAhj9sjPz9f/fr102OPPabJkydry5Yt+uMf/6iQkBDNmDFD0tXPb8XFxen777/XvHnz1KRJE23YsEGDBw8u1TYHUEkZALekJUuWGEkl/kRFRTlNExUVZXr37l3sPN966y0jyaxcudLRVty8e/bsaS5duuQ0/aJFi0xgYKCjT7169cyIESPMli1bnPr5+fmZ++67r0zrO2bMGCPJfPnll8YYY9LT040kM336dKd+9vb33nvPGGPMAw88YCIiIsyPP/5ojDFm5MiRJiAgoMRlnTt3zkgyr732Won9YmJiTGxsrEv7lClTjCSza9cup/Zx48YZm81mDh8+bIwxJiMjw0gy4eHhjvEZY8zFixdNzZo1TdeuXR1tgYGBZtKkSSWOx51hw4aZRo0aOV537drVPP7446ZGjRpm2bJlxhhj/v73vxtJ5tNPP3X0i42NdVq3s2fPGklm5syZLsuYOXOmkWTmz5/v1D5+/Hjj5+dnioqKShxjbGyskWQ++ugjp/bHH3/cVKlSxRw7dswYY8xf/vIXI8msWrXKqd+8efOcxr9gwQIjyWRnZxe7zN27dxtJZsmSJSWO7Vr29Xzqqaec2lNSUowk88477zjaoqKijJeXl2Nf2yUmJhpfX1+TmZnp1N6zZ09TrVo1x5hnz55tJJmNGzcWO57ly5cbSeb99993u27JycnGGGP+93//10gy+/fvL3ZeEyZMMNWrVy9h7f9zbKWnpzvaRo4c6Xaf9OrVyzRr1szx+s033zSSzCeffOLU73e/+12Z9wOAyoNbp4Bb3Ntvv63du3e7/Fx7RaK0TDFXQRISEhzz3bJli9544w3t2bNHPXr0cLo9YsyYMcrKytK7776rJ598UpGRkXrnnXcUGxurV155pdzreOnSJa1atUodO3ZUdHS0JCk2NlaNGzfW0qVLnW5Hut68efOUlZWl119/vdTLq1mzpho3bqxXXnlFf/rTn7Rv374Sl3G9tLQ0tWjRwvGBeLtRo0bJGOP4y7rdgAEDnD7UHhQUpL59+2rLli2OW4fat2+vpUuXas6cOdq5c6fy8/NLNZb4+HgdOXJEGRkZys3N1bZt29SjRw/FxcVp48aNkq5e5fD19S1XzVyrX79+Tq9btWql3NxcnTlzxuO0QUFBLtMPHTpURUVF2rJli6Sr2zUgIECDBg1y6me/hct+u5n9tqiEhAStWrWq2CeYuWP+ffXu2p/rPfroo06vExISVLVqVaWnpzu1t2rVSk2bNnVqS0tLU3x8vCIjI13W4cqVK44npX3yySdq2rSpunbtWuxY161bp+rVq6tv375O423Tpo3q1q3ruMWpTZs28vHx0W9/+1stW7ZMR44ccZlX+/btlZ2drSFDhuijjz4q0xPEbDaby+egWrVqpWPHjjleb968WUFBQerRo4dTvyFDhpR6OQAqH4IGcItr3ry52rVr5/Lj7rMPnth/MQgPD3dqr127tmO+nTt31hNPPKE33nhD27Ztc7m3OiQkREOGDNHrr7+uXbt26cCBA6pTp46mTp3quO2ofv36ysjIKPW4Vq5cqUuXLikhIUHZ2dnKzs7WhQsXlJCQoOPHjzt+YXanY8eO+vWvf62XX35ZP/zwQ6mWZ7PZ9Nlnn+mhhx7S/Pnz9Ytf/EK1a9fWk08+qZycHI/Tnz9/3u1tY/btev3tTnXr1nXpW7duXeXl5enSpUuSrm6DkSNH6n/+5390//33q2bNmhoxYoROnTpV4ljsv6impqZq27Ztys/P14MPPqiuXbs6fjFPTU1Vp06dbviDyqGhoU6v7beS/fjjjx6nrVOnjkubfbvYt9f58+dVt25d2Ww2p35hYWGqWrWqo9+vfvUrffjhhyooKNCIESMUERGhe+65R8uXL/c4js2bN8vb29vp5+jRo27HZVe1alWFhoa67Fd3NVDa2jh79qzHW7lOnz6t7Oxs+fj4uIz51KlTjrDQuHFjpaamKiwsTL///e/VuHFjNW7c2Cl8Dx8+XIsXL9axY8c0cOBAhYWFqUOHDiUeW3bVqlVzefqbr6+v02e9zp8/73Yfu2sDcOcgaAB3CGOM1q5dq4CAALVr185j/1atWkmS/vGPf5TYLyYmRomJicrPz9fXX38t6eqTqE6fPq2dO3eWamyLFi2SJE2aNEk1atRw/MydO9fp/eLMnTtXOTk5eumll0q1PEmKiorSokWLdOrUKR0+fFhPPfWUkpOT9cwzz3icNjQ0VCdPnnRpP3HihCSpVq1aTu3uwsKpU6fk4+Pj+M6PWrVq6bXXXtPRo0d17NgxzZ07V6tXr/b4aN2IiAg1bdpUqamp2rhxo9q1a6fq1asrPj5eJ0+e1K5du7Rz584S/3J+M5w+fdqlzb5d7AEmNDRUp0+fdrnydubMGRUUFDht1/79++uzzz7ThQsXtGnTJkVERGjo0KFuv1vlWm3btnW5Onh98L5+fxUUFOj8+fMuQev6QGRfh9LURu3atZWVlVXiWGvVqqXQ0FC3VzR3797t9Fmgzp07a+3atbpw4YJ27typ+++/X5MmTdKKFSscfUaPHq3t27frwoULWr9+vYwx6tOnj9OVifKy77vreQrKACo3ggZwh5g1a5YOHTqkiRMnlvjdFHb2py+FhYVJuvoXy7y8PLd97U/Jsf/C9tRTTykgIEDjx493+1QiY4zj8bZffvmlduzYoYEDByo9Pd3lJz4+Xh999FGJH4qOjo7WmDFj9Oc//1mZmZke1+16TZs21bRp09SyZUvt3bvX0e7r6+v2r/Xx8fE6dOiQU1/p6m1uNptNcXFxTu2rV692+utvTk6O1q5dq86dOzue6nWt+vXra8KECerWrZvLMtzp2rWr0tLStHHjRnXr1s2xTvXr19eMGTOUn5/vMWiU5epEeeTk5GjNmjVObe+++66qVKmiX/3qV5KubtdLly7pww8/dOr39ttvO96/nq+vr2JjYx2PbLZ/N0tx6xMUFORyddDHx8epT0pKitPrVatWqaCgoFTfLxIfH6+0tDRHsLh2HapVq+Z4QELPnj319ddfu9xmd60+ffro/PnzKiwsdHtVs1mzZi7TeHl5qUOHDnrzzTclyW39BAQEqGfPnpo6dary8vL0xRdfeFwvT2JjY5WTk6NPPvnEqf3aoAPgzsNTp4BKJjs723El4fLly44v7Nu6dasSEhIcj+C81rVXH3Jzc7V//37NmTNH1atXd3xnRnp6uiZOnKhHH31UHTt2VGhoqM6cOaPly5drw4YNjltYJKlhw4ZasWKFBg8erDZt2ji+sE+SDh06pMWLF8sYo4cffthxteLZZ591+cyDdPUX1M8++0zvvPOOJk6cWOx6JyUlKSUlRenp6QoICChxGx04cEATJkzQI488orvvvls+Pj5KS0vTgQMHNGXKFEe/li1basWKFVq5cqUaNWokPz8/tWzZUk899ZTefvtt9e7dW7Nnz1ZUVJTWr1+v5ORkjRs3zuW+fS8vL3Xr1k1PP/20ioqKNG/ePF28eNGxLy5cuKC4uDgNHTpU0dHRCgoK0u7du7Vhwwa3jwG+Xnx8vJKTk3Xu3Dm99tprTu1LlixRjRo1nB5t605QUJCioqL00UcfKT4+XjVr1lStWrVcvn2+vEJDQzVu3DhlZmaqadOm+vjjj/Xf//3fGjdunOrXry9JGjFihN58802NHDlSR48eVcuWLbVt2za99NJL6tWrlyMszZgxQ1lZWYqPj1dERISys7P1+uuvy9vb2/GlkY0bN5a/v79SUlLUvHlzBQYGKjw83OXqhTurV69W1apV1a1bN8dTp1q3bq2EhASP086cOVPr1q1TXFycZsyYoZo1ayolJUXr16/X/PnzHbc8Tpo0SStXrlT//v01ZcoUtW/fXj/++KM2b96sPn36KC4uTomJiUpJSVGvXr00ceJEtW/fXt7e3srKylJ6err69++vhx9+WH/5y1+Ulpam3r17q379+srNzXU8Ccy+zR5//HH5+/urU6dOqlevnk6dOqW5c+cqJCTE8ZmXGzFy5Ei9+uqrGjZsmObMmaMmTZrok08+0d/+9jdJzo+/BnAHqbjPoQMoif2pU7t373b7fu/evd0+dUr/fiKUzWYzgYGBplmzZmb48OHmb3/7m9v52Pvbf7y9vU2jRo3M6NGjzbfffuvod/z4cTNt2jTTqVMnU7duXVO1alUTFBRkOnToYP785z+bgoICl3l/9913Zvz48aZJkybG19fX+Pv7mxYtWpinn37aZGRkmLy8PBMWFmbatGlT7HYoKCgwERERpmXLlsYY16dOXeuFF14wkjw+der06dNm1KhRJjo62gQEBJjAwEDTqlUr8+qrrzqtx9GjR0337t1NUFCQy1O+jh07ZoYOHWpCQ0ONt7e3adasmXnllVdMYWGho4/9qVPz5s0zs2bNMhEREcbHx8fce++9TvsjNzfXjB071rRq1coEBwcbf39/06xZMzNz5kxz+fLlEtfFGGN++OEHU6VKFRMQEGDy8vIc7fanJQ0YMMBlmuufOmWMMampqebee+81vr6+RpIZOXKkMeY/T2M6e/asU397jWZkZJQ4vtjYWBMTE2M2bdpk2rVrZ3x9fU29evXMCy+8YPLz8536nj9/3owdO9bUq1fPVK1a1URFRZnnn3/e5ObmOvqsW7fO9OzZ09x1113Gx8fHhIWFmV69epmtW7c6zWv58uUmOjraeHt7F/tErWvZ1/Pzzz83ffv2NYGBgSYoKMgMGTLEnD592qlvSU94O3jwoOnbt68JCQkxPj4+pnXr1m6fuvTDDz+YiRMnmvr16xtvb28TFhZmevfubb766itHn/z8fLNgwQLTunVr4+fnZwIDA010dLT53e9+Z7755htjjDE7duwwDz/8sImKijK+vr4mNDTUxMbGmjVr1jjms2zZMhMXF2fq1KljfHx8THh4uElISDAHDhxw9CnuqVPujif7trpWZmamGTBggGO7DRw40Hz88cdunzgG4M5gM6YcD+MHAHh09OhRNWzYUK+88or+8Ic/VPRwKkyXLl107tw5/fOf/6zooZQoKSlJs2bN0tmzZ10+Z4PyeemllzRt2jRlZmaW+ntMAFQe3DoFAABumP2LF6Ojo5Wfn6+0tDS98cYbGjZsGCEDuEOVKmgUFRXpxIkTCgoKcvuUDQCAK/ujcnNzc3Xx4sUKHk3FKSwsVFFR0S2/DezfGZOTk+PyAXF4ZrPZlJycrMzMTP3000+KiIjQpEmT9Mwzz9zy+x5A2RhjlJOTo/Dw8BI/g1WqW6eysrJcvnwIAAAAwJ3r+PHjJV6xLNUVjaCgIMfMgoODrRnZLeS776TBgyUfH6kUT/3EHS43V8rLk1aulBo3rujRVCyOnbK5WbVzs/bLzT4WqLerOAdZy6q6qgz7hWPMOpWhHkpy8eJFRUZGOjJCcUoVNOy3SwUHB1fKoBEYKHl5SQEBUrVqFT0a3Oq8vKTCwqt1UwkPhzLh2Cmbm1U7N2u/3OxjgXq7inOQtayqq8qwXzjGrFMZ6qE0PH2kggdbAwAAALAcQQMAAACA5QgaAAAAACxH0AAAAABgOYIGAAAAAMsRNAAAAABYjqABAAAAwHIEDQAAAACWI2gAAAAAsBxBAwAAAIDlCBoAAAAALEfQAAAAAGA5ggYAAAAAyxE0AAAAAFiOoAEAAADAcgQNAAAAAJYjaAAAAACwHEEDAAAAgOUIGgAAAAAsR9AAAAAAYDmCxr9dvLjcbfvp08vd/rs41/fx9LqsbnR6q1g5jrLO6/Tp5cVO467dirHa52H/b3H1Yrd8+XI98cQTxb5X0uuS5nn9v8szbWn6LV++vMRprn3P3bYoy/4pzXulUVJd3Mi4Surr6fzgbkwXLy7XunWel1fafVbcNJ5qtLS+/vpqHV9/DJS0nOtrtSzr4mla+/KuHU9pztP29XDn5zgv/xzn6mvnWdL+Le/2Lq7t+tfFndusVJp1cHdOLO98PR0vpT233OhxV9bjvjznCU9Onbqx/XsjtW/VcVOe3ytuZPllOT/eaQga/1ZcMZw5s9ztv4tzfR9Pr8vqRqe3ipXjKOu8zpxZXuw07tqtGKt9Hvb/liZovPfee8W+V9LrkuZ5/b9v1aBRlv1TmvdKo6S6uJFxldTX0/nB3Zhut6Bx9uzVOr7+GChpOTcjaFw7ntKcp+3r4c7PcV7+Oc7V186zIoNGcec2K91qQaO055bKEDRycm5s/95I7Vt13JTn94obWX5Zzo93GoIGAAAAAMsRNAAAAABYjqABAAAAwHIEDQAAAACWI2gAAAAAsBxBAwAAAIDlCBoAAAAALEfQAAAAAGA5ggYAAAAAyxE0AAAAAFiOoAEAAADAcgQNAAAAAJYjaAAAAACwHEEDAAAAgOUIGgAAAAAsR9AAAAAAYDmCBgAAAADLETQAAAAAWI6gAQAAAMByBA0AAAAAliNoAAAAALAcQQMAAACA5QgaAAAAACxH0AAAAABgOYIGAAAAAMsRNAAAAABYjqABAAAAwHIEDQAAAACWI2gAAAAAsBxBAwAAAIDlCBoAAAAALEfQAAAAAGA5ggYAAAAAyxE0AAAAAFiOoAEAAADAcgQNAAAAAJYjaAAAAACwHEEDAAAAgOUIGgAAAAAsR9AAAAAAYDmCBgAAAADLETQAAAAAWI6gAQAAAMByBA0AAAAAliNoAAAAALAcQQMAAACA5QgaAAAAACxH0AAAAABgOYIGAAAAAMsRNAAAAABYjqABAAAAwHIEDQAAAACWI2gAAAAAsBxBAwAAAIDlCBoAAAAALEfQAAAAAGA5ggYAAAAAyxE0AAAAAFiOoAEAAADAcgQNAAAAAJYjaAAAAACwHEEDAAAAgOUIGgAAAAAsR9AAAAAAYDmCBgAAAADLETQAAAAAWI6gAQAAAMByVSt6ALeK4OAhbtvDwoa4/Xdxru/j6XVZ3ej0VrFyHGWdV0n93b1nxVjt87D/t7h6sRsyZIiioqKKfa+k1yXN8/p/l2fa0vTz1P/a991ti+K2eVn3XVmU5/gsz7KLOyeUtvaCg4eoTx/PyyntPituGk81Wlq1az8iyfUYKGk57mq1tDxNa19eceMpbl/a18Odn6P2fo5z9bXzLGn/lmWbu+vr6Rz1yCPFb0urlGYdylNnxfXzdLx42p+l/X+DJzdyvFglKOjG9u+N1L5Vx015f68o7/LLcn6809iMMcZTp4sXLyokJEQXLlxQcHDwzRjXTfXNN1K/flJwsFStWkWPBre6K1ekixelNWuku++u6NFULI6dsrlZtXOz9svNPhaot6s4B1nLqrqqDPuFY8w6laEeSlLabMCtUwAAAAAsR9AAAAAAYDmCBgAAAADLETQAAAAAWI6gAQAAAMByBA0AAAAAliNoAAAAALAcQQMAAACA5QgaAAAAACxH0AAAAABgOYIGAAAAAMsRNAAAAABYjqABAAAAwHIEDQAAAACWI2gAAAAAsBxBAwAAAIDlCBoAAAAALEfQAAAAAGA5ggYAAAAAyxE0AAAAAFiuamk6GWMkSRcvXvxZB1NRLl2SCguly5ev/hcoSW7u1Tq5dEmqpIdEqXHslM3Nqp2btV9u9rFAvV3FOchaVtVVZdgvHGPWqQz1UBJ7JrBnhOLYjKcekrKyshQZGWnNyAAAAADc9o4fP66IiIhi3y9V0CgqKtKJEycUFBQkm81m6QArwsWLFxUZGanjx48rODi4oocDuKBGcSujPnGro0ZxK6sM9WmMUU5OjsLDw1WlSvGfxCjVrVNVqlQpMa3croKDg2/bHYw7AzWKWxn1iVsdNYpb2e1enyEhIR778GFwAAAAAJYjaAAAAACw3B0ZNHx9fTVz5kz5+vpW9FAAt6hR3MqoT9zqqFHcyu6k+izVh8EBAAAAoCzuyCsaAAAAAH5eBA0AAAAAliNoAAAAALAcQQMAAACA5W6boLFlyxb17dtX4eHhstls+vDDD136rF69Wg899JBq1aolm82m/fv3l3r+WVlZ8vHxUXR0tNv3bTabbDabdu7c6dT+008/KTQ0VDabTZs2bSrDGqGy8VSj+fn5eu6559SyZUsFBAQoPDxcI0aM0IkTJ0qcb1JSkqP+vLy8FBkZqd/85jc6e/asow/1idJITk5Ww4YN5efnp7Zt22rr1q1O75fnHEp9wkol1SjnUFQ0T+fQpKQkRUdHKyAgQDVq1FDXrl21a9euEudZ2evztgkaly9fVuvWrbVw4cIS+3Tq1Ekvv/xymee/dOlSJSQk6MqVK/r73//utk9kZKSWLFni1PbBBx8oMDCwzMtD5eOpRq9cuaK9e/dq+vTp2rt3r1avXq2vv/5a/fr18zjvmJgYnTx5UpmZmXrrrbe0du1ajRgxwqkP9YmSrFy5UpMmTdLUqVO1b98+de7cWT179lRmZqajT3nPodQnrOCpRjmHoiKV5hzatGlTLVy4UAcPHtS2bdvUoEEDde/e3Sk0uFOp69PchiSZDz74oNj3MzIyjCSzb9++Us2vqKjINGrUyGzYsME899xzZvTo0W6XOW3aNBMcHGyuXLniaO/WrZuZPn26kWTS09PLuCaorDzVqN3//d//GUnm2LFjxfaZOXOmad26tVPbnDlzTJUqVRy1SH3Ck/bt25uxY8c6tUVHR5spU6a49C3LOZT6hFXKUqN2nENxs5SnPi9cuGAkmdTU1GL7VPb6vG2uaPyc0tPTdeXKFXXt2lXDhw/XqlWrlJOT49Kvbdu2atiwod5//31J0vHjx7VlyxYNHz78Zg8ZlcSFCxdks9lUvXr1Mk3n7++voqIiFRQUONqoTxQnLy9Pn3/+ubp37+7U3r17d23fvt3y5VGfKKvy1ijnUNwM5anPvLw8/fWvf1VISIhat25dpuVVpvokaEhatGiREhMT5eXlpZiYGDVp0kQrV65023f06NFavHixJGnJkiXq1auXateufTOHi0oiNzdXU6ZM0dChQxUcHFzq6b766iu99dZbat++vYKCgpzeoz7hzrlz51RYWKg6deo4tdepU0enTp2ydFnUJ8qjPDXKORQ3S1nqc926dQoMDJSfn59effVVbdy4UbVq1Sr1sipbfd7xQSM7O1urV6/WsGHDHG3Dhg1z7MjrDRs2TDt27NCRI0e0dOlSjRkz5mYNFZVIfn6+EhMTVVRUpOTkZI/9Dx48qMDAQPn7+6tFixaKjIxUSkqKSz/qEyWx2WxOr40xLm3lQX3CKqWtUc6hqAilqc+4uDjt379f27dvV48ePZSQkKAzZ86UON/KXJ9VK3oAFe3dd99Vbm6uOnTo4GgzxqioqEiHDh1SixYtnPqHhoaqT58+euyxx5Sbm6uePXu6vc0KKE5+fr4SEhKUkZGhtLS0Uv0lrlmzZlqzZo28vLwUHh4uX19ft/2oT7hTq1YteXl5ufzl7cyZMy5/oSsP6hM3qiw1yjkUN1tZ6jMgIEBNmjRRkyZNdN999+nuu+/WokWL9Pzzzxc7/8pcn3f8FY1FixZp8uTJ2r9/v+PnH//4h+Li4oq9qjFmzBht2rRJI0aMkJeX100eMW5n9v9BfvPNN0pNTVVoaGippvPx8VGTJk3UsGHDYk9AdtQnrufj46O2bdtq48aNTu0bN25Ux44dLZk/9YkbUdoa5RyKinAj51BjjH766SeP86+s9XnbXNG4dOmSvv32W8frjIwM7d+/XzVr1lT9+vUlSd9//70yMzMdz9Q+fPiwJKlu3bqqW7euyzz379+vvXv3KiUlxeX7M4YMGaKpU6dq7ty58vb2dnqvR48eOnv2bJnuCUXl56lGCwoKNGjQIO3du1fr1q1TYWGh468jNWvWlI+PjyXjoD7hztNPP63hw4erXbt2uv/++/XXv/5VmZmZGjt2rKNPWc+h5UF9ojieapRzKCqSp/q8fPmyXnzxRfXr10/16tXT+fPnlZycrKysLD3yyCOWjeN2q8/bJmjs2bNHcXFxjtdPP/20JGnkyJFaunSpJGnNmjUaPXq0o09iYqIkaebMmUpKSnKZ56JFi9SiRQu3X9L361//WuPGjdPatWs1YMAAp/dsNluZPtiDO4OnGs3KytKaNWskSW3atHGaNj09XV26dLFkHNQn3Bk8eLDOnz+v2bNn6+TJk7rnnnv08ccfKyoqytGnrOfQ8qA+URxPNco5FBXJU316eXnpq6++0rJly3Tu3DmFhobql7/8pbZu3aqYmBjLxnG71afNGGMqehAAAAAAKpc7/jMaAAAAAKxH0AAAAABgOYIGAAAAAMsRNAAAAABYjqABAAAAwHIEDQAAAACWI2gAAAAAsBxBAwAAAIDlCBoAAAAALEfQAAAAAGA5ggYAAAAAyxE0AAAAAFju/wES81BtVy2jVQAAAABJRU5ErkJggg==",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "execution_count": 2,
+ "id": "ca45c6c3",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:33.744689Z",
+ "iopub.status.busy": "2025-11-24T18:32:33.743614Z",
+ "iopub.status.idle": "2025-11-24T18:32:34.044704Z",
+ "shell.execute_reply": "2025-11-24T18:32:34.044704Z"
}
- ],
+ },
+ "outputs": [],
"source": [
- "fig, ax_barcode = plt.subplots(figsize=(10,1.5))\n",
+ "fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),\n",
+ " gridspec_kw={'height_ratios':[10,1]})\n",
+ "\n",
+ "gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')\n",
+ "city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')\n",
+ "\n",
+ "plot_stops(stops_hdb, ax=ax_map, cmap='Blues')\n",
+ "plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)\n",
+ "ax_map.set_axis_off()\n",
"\n",
"plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops_hdb, ax=ax_barcode, stop_color='blue', set_xlim=False, timestamp='unix_ts')\n",
- "fig.suptitle(\"HDBSCAN stops with post-processing\")\n",
+ "plot_stops_barcode(stops_hdb, ax=ax_barcode, cmap='Blues', set_xlim=False, timestamp='unix_ts')\n",
+ "\n",
+ "plt.tight_layout(pad=0.1)\n",
"plt.show()"
]
}
@@ -105,7 +111,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.5"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/docs/source/ingesting-data.ipynb b/docs/source/ingesting-data.ipynb
new file mode 100644
index 00000000..0f7b355e
--- /dev/null
+++ b/docs/source/ingesting-data.ipynb
@@ -0,0 +1,507 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "460ff464-7812-41fb-bc5b-bc4f24e16499",
+ "metadata": {
+ "id": "460ff464-7812-41fb-bc5b-bc4f24e16499"
+ },
+ "source": [
+ "# **Loading and Sampling Trajectory Data**\n",
+ "\n",
+ "## Getting started\n",
+ "\n",
+ "Real-world mobility files vary widely in structure and formatting:\n",
+ "- e.g. **Timestamps** may be **UNIX** integers or **ISO-formatted strings**\n",
+ "- May have **timezones**, e.g. -05:00, Z, (GMT+01), -3600\n",
+ "- Coordinates might be **projected** or **geographical**\n",
+ "- Files may be a flat **CSV**, or **partitioned Parquets**, local or **in S3**.\n",
+ "\n",
+ "`nomad.io` is here to help."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "ca448248-3077-4e67-ad81-6d1ba1b170db",
+ "metadata": {
+ "executionInfo": {
+ "elapsed": 3404,
+ "status": "ok",
+ "timestamp": 1753083319439,
+ "user": {
+ "displayName": "Thomas Li",
+ "userId": "03526318197962168317"
+ },
+ "user_tz": -120
+ },
+ "id": "ca448248-3077-4e67-ad81-6d1ba1b170db"
+ },
+ "outputs": [],
+ "source": [
+ "from nomad.io import base as loader\n",
+ "import pandas as pd\n",
+ "import geopandas as gpd"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c78e81f2-bcf3-4cc5-8c26-b6111484df73",
+ "metadata": {
+ "id": "c78e81f2-bcf3-4cc5-8c26-b6111484df73"
+ },
+ "source": [
+ "## Typical data ingestion ( `pandas`, `geopandas`) vs `nomad` `io` utilities"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "904bf840-4253-41e3-a1d3-d54874072613",
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 206
+ },
+ "executionInfo": {
+ "elapsed": 849,
+ "status": "ok",
+ "timestamp": 1753083322765,
+ "user": {
+ "displayName": "Thomas Li",
+ "userId": "03526318197962168317"
+ },
+ "user_tz": -120
+ },
+ "id": "904bf840-4253-41e3-a1d3-d54874072613",
+ "outputId": "9dd16f0f-7e96-4aaa-ade3-c431698bafbc"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " identifier \n",
+ " device_lon \n",
+ " device_lat \n",
+ " unix_timestamp \n",
+ " local_datetime \n",
+ " date \n",
+ " ha \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 \n",
+ " cocky_stallman \n",
+ " -38.318802 \n",
+ " 36.669894 \n",
+ " 1704114435 \n",
+ " 2024-01-01 09:07:15-04:00 \n",
+ " 2024-01-01 \n",
+ " 8.492856 \n",
+ " \n",
+ " \n",
+ " 1 \n",
+ " cocky_stallman \n",
+ " -38.318765 \n",
+ " 36.669905 \n",
+ " 1704114753 \n",
+ " 2024-01-01 09:12:33-04:00 \n",
+ " 2024-01-01 \n",
+ " 11.336772 \n",
+ " \n",
+ " \n",
+ " 2 \n",
+ " cocky_stallman \n",
+ " -38.318627 \n",
+ " 36.669856 \n",
+ " 1704114792 \n",
+ " 2024-01-01 09:13:12-04:00 \n",
+ " 2024-01-01 \n",
+ " 18.436612 \n",
+ " \n",
+ " \n",
+ " 3 \n",
+ " cocky_stallman \n",
+ " -38.318661 \n",
+ " 36.669920 \n",
+ " 1704114989 \n",
+ " 2024-01-01 09:16:29-04:00 \n",
+ " 2024-01-01 \n",
+ " 27.370737 \n",
+ " \n",
+ " \n",
+ " 4 \n",
+ " cocky_stallman \n",
+ " -38.318602 \n",
+ " 36.669823 \n",
+ " 1704115195 \n",
+ " 2024-01-01 09:19:55-04:00 \n",
+ " 2024-01-01 \n",
+ " 12.506606 \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " identifier device_lon device_lat unix_timestamp \\\n",
+ "0 cocky_stallman -38.318802 36.669894 1704114435 \n",
+ "1 cocky_stallman -38.318765 36.669905 1704114753 \n",
+ "2 cocky_stallman -38.318627 36.669856 1704114792 \n",
+ "3 cocky_stallman -38.318661 36.669920 1704114989 \n",
+ "4 cocky_stallman -38.318602 36.669823 1704115195 \n",
+ "\n",
+ " local_datetime date ha \n",
+ "0 2024-01-01 09:07:15-04:00 2024-01-01 8.492856 \n",
+ "1 2024-01-01 09:12:33-04:00 2024-01-01 11.336772 \n",
+ "2 2024-01-01 09:13:12-04:00 2024-01-01 18.436612 \n",
+ "3 2024-01-01 09:16:29-04:00 2024-01-01 27.370737 \n",
+ "4 2024-01-01 09:19:55-04:00 2024-01-01 12.506606 "
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df = pd.read_csv(\"../../tutorials/IC2S2-2025/IC2S2-2025/gc_data.csv\")\n",
+ "city = gpd.read_file(\"../../tutorials/IC2S2-2025/IC2S2-2025/garden_city.geojson\")\n",
+ "\n",
+ "df.head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "de10f93d",
+ "metadata": {},
+ "source": [
+ "## `nomad.io` — facilitates type casting and default names\n",
+ "\n",
+ "`nomad.io.base.from_file` is basically a `pandas` / `pyarrow` wrapper, trying to simplify the formatting of canonical variables\n",
+ "\n",
+ "- dates and datetimes in **ISO format** are cast to `pandas.datetime64`\n",
+ "- **unix timestamps** are cast to integers and **reformatted to seconds**.\n",
+ "- **user identifiers** are cast to strings\n",
+ "- **partition folders** can be read as columns (Hive)\n",
+ "- **timezone handling** parses ISO datetime strings (with or without timezones)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "03b7bf33-48a1-4d75-bd95-ae05fb7f9357",
+ "metadata": {
+ "id": "03b7bf33-48a1-4d75-bd95-ae05fb7f9357"
+ },
+ "source": [
+ "Don't read partitioned data with a for loop! `nomad`'s `from_file` wraps `PyArrow`'s file readers maintaning the same signature."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "b33de9d2-ee49-46ac-96a1-56784674d40c",
+ "metadata": {
+ "id": "b33de9d2-ee49-46ac-96a1-56784674d40c"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "user_id object\n",
+ "longitude float64\n",
+ "latitude float64\n",
+ "timestamp Int64\n",
+ "datetime datetime64[ns]\n",
+ "ha float64\n",
+ "date object\n",
+ "tz_offset Int64\n",
+ "dtype: object\n"
+ ]
+ }
+ ],
+ "source": [
+ "# For the partitioned dataset\n",
+ "traj_cols = {\"user_id\": \"user_id\",\n",
+ " \"timestamp\": \"timestamp\",\n",
+ " \"latitude\": \"latitude\",\n",
+ " \"longitude\": \"longitude\",\n",
+ " \"datetime\": \"datetime\",\n",
+ " \"date\": \"date\"}\n",
+ "\n",
+ "file_path = \"../../tutorials/IC2S2-2025/IC2S2-2025/gc_data/\" # partitioned\n",
+ "\n",
+ "\n",
+ "df = loader.from_file(file_path, format=\"csv\", traj_cols=traj_cols, parse_dates=True)\n",
+ "print(df.dtypes)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "8ca7f977",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Canonical column names in nomad\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "{'user_id': 'user_id',\n",
+ " 'latitude': 'latitude',\n",
+ " 'longitude': 'longitude',\n",
+ " 'datetime': 'datetime',\n",
+ " 'start_datetime': 'start_datetime',\n",
+ " 'end_datetime': 'end_datetime',\n",
+ " 'start_timestamp': 'start_timestamp',\n",
+ " 'end_timestamp': 'end_timestamp',\n",
+ " 'timestamp': 'timestamp',\n",
+ " 'date': 'date',\n",
+ " 'utc_date': 'date',\n",
+ " 'x': 'x',\n",
+ " 'y': 'y',\n",
+ " 'geohash': 'geohash',\n",
+ " 'tz_offset': 'tz_offset',\n",
+ " 'duration': 'duration',\n",
+ " 'ha': 'ha',\n",
+ " 'h3_cell': 'h3_cell',\n",
+ " 'location_id': 'location_id'}"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from nomad.constants import DEFAULT_SCHEMA\n",
+ "print(\"Canonical column names in nomad\")\n",
+ "DEFAULT_SCHEMA"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cf4fbb1f",
+ "metadata": {},
+ "source": [
+ "```from_file``` automatically detects and reads Parquet files (single or partitioned directories) using ```PyArrow```'s dataset API, applying the same validation, type casting, and timezone handling as for CSV inputs."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "da9e025d",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "uid object\n",
+ "timestamp Int64\n",
+ "latitude float64\n",
+ "longitude float64\n",
+ "date object\n",
+ "dtype: object\n"
+ ]
+ }
+ ],
+ "source": [
+ "traj_cols = {\"user_id\": \"uid\",\n",
+ " \"timestamp\": \"timestamp\",\n",
+ " \"latitude\": \"latitude\",\n",
+ " \"longitude\": \"longitude\",\n",
+ " \"date\": \"date\"}\n",
+ "\n",
+ "file_path = \"../../nomad/data/partitioned_parquet/\" # partitioned\n",
+ "\n",
+ "df = loader.from_file(file_path, format=\"parquet\", traj_cols=traj_cols, parse_dates=True)\n",
+ "print(df.dtypes)"
+ ]
+ }
+ ],
+ "metadata": {
+ "colab": {
+ "provenance": []
+ },
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.3"
+ },
+ "nbdime-conflicts": {
+ "local_diff": [
+ {
+ "diff": [
+ {
+ "diff": [
+ {
+ "key": 0,
+ "op": "addrange",
+ "valuelist": [
+ "conda_py_310_env"
+ ]
+ },
+ {
+ "key": 0,
+ "length": 1,
+ "op": "removerange"
+ }
+ ],
+ "key": "display_name",
+ "op": "patch"
+ },
+ {
+ "diff": [
+ {
+ "key": 0,
+ "op": "addrange",
+ "valuelist": [
+ "conda_py_310_env"
+ ]
+ },
+ {
+ "key": 0,
+ "length": 1,
+ "op": "removerange"
+ }
+ ],
+ "key": "name",
+ "op": "patch"
+ }
+ ],
+ "key": "kernelspec",
+ "op": "patch"
+ },
+ {
+ "diff": [
+ {
+ "diff": [
+ {
+ "diff": [
+ {
+ "key": 5,
+ "op": "addrange",
+ "valuelist": "6"
+ },
+ {
+ "key": 5,
+ "length": 2,
+ "op": "removerange"
+ }
+ ],
+ "key": 0,
+ "op": "patch"
+ }
+ ],
+ "key": "version",
+ "op": "patch"
+ }
+ ],
+ "key": "language_info",
+ "op": "patch"
+ }
+ ],
+ "remote_diff": [
+ {
+ "diff": [
+ {
+ "diff": [
+ {
+ "key": 0,
+ "op": "addrange",
+ "valuelist": [
+ "Python 3 (ipykernel)"
+ ]
+ },
+ {
+ "key": 0,
+ "length": 1,
+ "op": "removerange"
+ }
+ ],
+ "key": "display_name",
+ "op": "patch"
+ },
+ {
+ "diff": [
+ {
+ "key": 0,
+ "op": "addrange",
+ "valuelist": [
+ "python3"
+ ]
+ },
+ {
+ "key": 0,
+ "length": 1,
+ "op": "removerange"
+ }
+ ],
+ "key": "name",
+ "op": "patch"
+ }
+ ],
+ "key": "kernelspec",
+ "op": "patch"
+ },
+ {
+ "diff": [
+ {
+ "diff": [
+ {
+ "key": 0,
+ "length": 1,
+ "op": "removerange"
+ }
+ ],
+ "key": "version",
+ "op": "patch"
+ }
+ ],
+ "key": "language_info",
+ "op": "patch"
+ }
+ ]
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/source/lachesis_demo.ipynb b/docs/source/lachesis_demo.ipynb
index a46d838b..cb9a8992 100644
--- a/docs/source/lachesis_demo.ipynb
+++ b/docs/source/lachesis_demo.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
- "id": "92838936",
+ "id": "05c3afed",
"metadata": {},
"source": [
"# Lachesis Stop Detection"
@@ -10,7 +10,7 @@
},
{
"cell_type": "markdown",
- "id": "cb276fd9",
+ "id": "bca8605a",
"metadata": {},
"source": [
"The first stop detection algorithm implemented in ```nomad``` is a sequential algorithm insipired by the one in _Project Lachesis: Parsing and Modeling Location Histories_ (Hariharan & Toyama). This algorithm for extracting stays is dependent on two parameters: the roaming distance and the stay duration. \n",
@@ -28,32 +28,43 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "19184dee",
- "metadata": {},
+ "execution_count": 1,
+ "id": "7f0b2bb1",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:46.921799Z",
+ "iopub.status.busy": "2025-11-24T18:32:46.921799Z",
+ "iopub.status.idle": "2025-11-24T18:32:51.000857Z",
+ "shell.execute_reply": "2025-11-24T18:32:51.000857Z"
+ }
+ },
"outputs": [],
"source": [
"%matplotlib inline\n",
+ "import matplotlib\n",
+ "matplotlib.use('TkAgg')\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.ion()\n",
"\n",
"# Imports\n",
"import nomad.io.base as loader\n",
+ "import geopandas as gpd\n",
"from shapely.geometry import box\n",
- "import matplotlib.pyplot as plt\n",
- "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode\n",
+ "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_stops, plot_pings\n",
"import nomad.stop_detection.lachesis as LACHESIS\n",
+ "import nomad.data as data_folder\n",
+ "from pathlib import Path\n",
"\n",
"# Load data\n",
+ "data_dir = Path(data_folder.__file__).parent\n",
+ "city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')\n",
+ "outer_box = box(*city.total_bounds)\n",
+ "\n",
"filepath_root = 'gc_data_long/'\n",
- "tc = {\n",
- " \"user_id\": \"gc_identifier\",\n",
- " \"timestamp\": \"unix_ts\",\n",
- " \"x\": \"dev_x\",\n",
- " \"y\": \"dev_y\",\n",
- " \"ha\":\"ha\",\n",
- " \"date\":\"date\"}\n",
+ "tc = {\"user_id\": \"gc_identifier\", \"x\": \"dev_x\", \"y\": \"dev_y\", \"timestamp\": \"unix_ts\"}\n",
"\n",
"users = ['admiring_brattain']\n",
- "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','==', '2024-01-01'), traj_cols=tc)\n",
+ "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)\n",
"\n",
"# Lachesis (sequential stop detection)\n",
"stops = LACHESIS.lachesis(traj, delta_roam=20, dt_max = 60, dur_min=5, complete_output=True, keep_col_names=True, traj_cols=tc)"
@@ -61,28 +72,32 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "570b6103",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "execution_count": 2,
+ "id": "86273598",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:51.005525Z",
+ "iopub.status.busy": "2025-11-24T18:32:51.000857Z",
+ "iopub.status.idle": "2025-11-24T18:32:51.355886Z",
+ "shell.execute_reply": "2025-11-24T18:32:51.355274Z"
}
- ],
+ },
+ "outputs": [],
"source": [
- "fig, ax_barcode = plt.subplots(figsize=(10,1.5))\n",
+ "fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),\n",
+ " gridspec_kw={'height_ratios':[10,1]})\n",
+ "\n",
+ "gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')\n",
+ "city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')\n",
+ "\n",
+ "plot_stops(stops, ax=ax_map, cmap='Blues')\n",
+ "plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)\n",
+ "ax_map.set_axis_off()\n",
+ "\n",
+ "plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)\n",
+ "plot_stops_barcode(stops, ax=ax_barcode, cmap='Blues', set_xlim=False, timestamp='unix_ts')\n",
"\n",
- "plot_time_barcode(traj[tc['timestamp']], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops, ax=ax_barcode, stop_color='blue', set_xlim=False, timestamp='unix_ts')\n",
- "fig.suptitle(\"Lachesis stops\")\n",
- "plt.tight_layout()\n",
+ "plt.tight_layout(pad=0.1)\n",
"plt.show()"
]
}
@@ -103,7 +118,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.5"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/docs/source/tadbscan_demo.ipynb b/docs/source/tadbscan_demo.ipynb
index 6e28d6b4..c4f5747b 100644
--- a/docs/source/tadbscan_demo.ipynb
+++ b/docs/source/tadbscan_demo.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
- "id": "92838936",
+ "id": "7c76d1d8",
"metadata": {},
"source": [
"# TADBSCAN Stop Detection"
@@ -10,7 +10,7 @@
},
{
"cell_type": "markdown",
- "id": "cb276fd9",
+ "id": "f29a96ce",
"metadata": {},
"source": [
"The second stop detection algorithm implemented in ```nomad``` is an adaptation of DBSCAN. Unlike in plain DBSCAN, we also incorporate the time dimension to determine if two pings are \"neighbors\". This implementation relies on 3 parameters\n",
@@ -24,79 +24,81 @@
},
{
"cell_type": "code",
- "execution_count": 4,
- "id": "19184dee",
- "metadata": {},
+ "execution_count": 1,
+ "id": "1e62c25a",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:03.240035Z",
+ "iopub.status.busy": "2025-11-24T18:33:03.240035Z",
+ "iopub.status.idle": "2025-11-24T18:33:05.816985Z",
+ "shell.execute_reply": "2025-11-24T18:33:05.816985Z"
+ }
+ },
"outputs": [],
"source": [
"%matplotlib inline\n",
+ "import matplotlib\n",
+ "matplotlib.use('TkAgg')\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.ion()\n",
"\n",
"# Imports\n",
"import nomad.io.base as loader\n",
"import geopandas as gpd\n",
"from shapely.geometry import box\n",
- "import pandas as pd\n",
- "import numpy as np\n",
- "import matplotlib.pyplot as plt\n",
- "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode\n",
+ "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_stops, plot_pings\n",
"import nomad.stop_detection.dbscan as DBSCAN\n",
- "import nomad.filters as filters \n",
- "import nomad.stop_detection.postprocessing as post\n",
"\n",
"# Load data\n",
- "city = gpd.read_file(\"garden_city.geojson\").to_crs('EPSG:3857')\n",
- "outer_box = box(*city.total_bounds).buffer(15, join_style='mitre')\n",
+ "import nomad.data as data_folder\n",
+ "from pathlib import Path\n",
+ "data_dir = Path(data_folder.__file__).parent\n",
+ "city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')\n",
+ "outer_box = box(*city.total_bounds)\n",
"\n",
"filepath_root = 'gc_data_long/'\n",
- "tc = {\n",
- " \"user_id\": \"gc_identifier\",\n",
- " \"timestamp\": \"unix_ts\",\n",
- " \"x\": \"dev_x\",\n",
- " \"y\": \"dev_y\",\n",
- " \"ha\":\"ha\",\n",
- " \"date\":\"date\"}\n",
+ "tc = {\"user_id\": \"gc_identifier\", \"x\": \"dev_x\", \"y\": \"dev_y\", \"timestamp\": \"unix_ts\"}\n",
"\n",
"# Density based stop detection (Temporal DBSCAN)\n",
- "users = ['confident_aryabhata']\n",
- "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','<=', '2024-01-03'), traj_cols=tc)\n",
- "traj[['longitude','latitude']] = np.column_stack(\n",
- " filters.to_projection(traj, x='dev_x', y='dev_y', data_crs='EPSG:3857', crs_to='EPSG:4326')\n",
- ")\n",
+ "users = ['admiring_brattain']\n",
+ "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)\n",
"\n",
- "user_data_tadb = traj.assign(cluster=DBSCAN.ta_dbscan_labels(traj, time_thresh=240, dist_thresh=15, min_pts=3, traj_cols=tc))\n",
"stops_tadb = DBSCAN.ta_dbscan(traj,\n",
" time_thresh=720,\n",
" dist_thresh=15,\n",
" min_pts=3,\n",
" complete_output=True,\n",
- " traj_cols=tc)\n",
- "stops_tadb[\"cluster\"] = post.remove_overlaps(user_data_tadb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3) "
+ " traj_cols=tc) "
]
},
{
"cell_type": "code",
- "execution_count": 5,
- "id": "2159107b",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAA9oAAACMCAYAAACDKNEwAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAp9ElEQVR4nO3deXxU1d3H8e9kyGQhCxACJRC2qGwxWFbBUnaRTQXUAsoiqPUpYkFUtC4Ea4uIIlgU9DEsigZEAQUECgjiEhAr2AcBl0oEhCgGSSIQkOQ8f9C5zJpMwmQI8nm/XvOCOXPuOeeee+Y385u5c2MzxhgBAAAAAICgCDvfAwAAAAAA4NeERBsAAAAAgCAi0QYAAAAAIIhItAEAAAAACCISbQAAAAAAgohEGwAAAACAICLRBgAAAAAgiEi0AQAAAAAIIhJtAAAAAACCiEQbAMrAZrMFdNu0aZO1zbPPPiubzabU1NQy9bVp0ya3Nh0OhxITE3XVVVfpoYce0rfffuu1zfz5873GkpiYqC5dumjlypVe9Xfv3q1hw4apcePGioyMVM2aNdWqVSvdddddys/P96q/YsUK9e/fX7Vr15bD4VCNGjXUvXt3vfrqq/rll1+86v/444+KiIiQzWbTJ5984nM/R44cKZvNphYtWqioqMjrcZvNprvuuiuQKSvRwYMHlZ6erh07dpxzW5VNdna2bDab5s+fb5V99NFHSk9P19GjR73qN2zYUP369QvdAIPgnXfeUXp6+vkexgVl5MiRatiw4fkeBgBclEi0AaAMsrKy3G59+vRRVFSUV3mrVq2sbebOnStJ+vzzz7V169Yy9/n3v/9dWVlZ2rhxozIyMtSlSxfNnTtXzZo106uvvupzm3nz5ikrK0sfffSRXnzxRdntdvXv318rVqyw6mzfvl2tW7fWrl279Oijj2rNmjWaM2eO+vbtq7Vr1+rIkSNWXWOMbr31Vl177bUqLi7W9OnTtX79ei1YsEAtW7bUn/70Jz3//PNe43jllVd06tQpSVJGRkaJ+7lr1y63RDHYDh48qMmTJ/8qE+06deooKytLffv2tco++ugjTZ482WeifSF65513NHny5PM9jAvKI488omXLlp3vYQDARanK+R4AAFxIrrzySrf7iYmJCgsL8yp3+uSTT/TZZ5+pb9++WrVqlTIyMtS+ffsy9XnppZe6tX/ttddqwoQJ6tGjh0aOHKm0tDRdfvnlbtukpqaqTZs21v1rrrlG1atXV2Zmpvr37y9JmjFjhsLCwrRp0ybFxsZadW+44Qb99a9/lTHGKps2bZrmz5+vyZMn69FHH3Xrq3///rr//vv19ddfe4197ty5qlWrlho0aKDMzExNnz5dUVFRXvWqVq2qVq1aadKkSRo6dKjPOvAvIiLC7xpE+fzyyy+y2WyqUuXCfauUkpJyvocAABctvtEGgArk/Bb3iSeeUMeOHbVo0SIdP378nNutUaOGXnjhBZ0+fVrPPPNMqfUjIyPlcDgUHh5uleXm5iouLk4xMTE+t7HZbJLOJBxTp05V06ZN9cgjj/is+5vf/Ea/+93v3Mq2bt2qnTt3atiwYbr99tuVl5enN9980+8Yp06dqu+++04zZ84sdX98WbJkidq3b6/4+HhFR0ercePGGjVqlKQzp+G3bdtWknTrrbdap9W7nor89ttvq0OHDoqOjlZsbKx69uyprKwstz7S09Nls9m0fft2DRw4UHFxcYqPj9ctt9yiw4cPu9V999131aVLFyUkJCgqKkr169fXoEGDSjz+9913n+Lj491OoR87dqxsNpumTZtmleXm5iosLEz/+Mc/JHmfOp6enq777rtPktSoUSOfP2mQpDVr1qhVq1aKiopS06ZNrbMvSuLs68knn9Tf/vY31a9fX5GRkWrTpo02bNjgVf+DDz5Q9+7dFRsbq+joaHXs2FGrVq1yq3P8+HHde++9atSokSIjI1WjRg21adNGmZmZks6cAv3cc89Jcv/5RnZ2doljdZ4iv2zZMqWlpSkyMlKNGzfWs88+61bP+TONV155RRMmTFDdunUVERFhfXg0d+5ctWzZ0hrbgAEDtHv3bq/+tm7dqv79+yshIUGRkZFKSUnRuHHj3Op89dVXGjp0qGrVqqWIiAg1a9bM2jen4uJiPf7442rSpImioqJUrVo1paWluT03Dh8+rDvuuEPJycmKiIiwflayfv16q46vU8edP8V45ZVX1KxZM0VHR6tly5Y+f1ry1ltvKS0tTREREWrcuLFmzpxpPQcAACUj0QaACnLixAllZmaqbdu2Sk1N1ahRo1RQUKAlS5YEpf22bduqTp062rx5s9djRUVFOn36tH755RcdOHBA48aN07FjxzR06FCrTocOHXTo0CHdfPPNeu+993TixAmf/XzyySc6cuSIrrvuujK9wXZ+yDBq1CgNHjxY0dHRJZ4+3qFDBw0YMEBTp051O209EFlZWfrDH/6gxo0ba9GiRVq1apUeffRRnT59WpLUqlUrzZs3T5L08MMPW6f433bbbZKk1157Tdddd53i4uKUmZmpjIwM/fTTT+rSpYs++OADr/4GDBigSy65RG+88YbS09O1fPly9erVy/qdenZ2tvr27SuHw6G5c+dqzZo1euKJJ1S1alXrVHpfevToofz8fH388cdW2fr16xUVFaV169ZZZRs2bJAxRj169PDZzm233aaxY8dKkpYuXerzJw2fffaZJkyYoPHjx1sJ1ejRo32uJ19mzZqlNWvWaMaMGVq4cKHCwsLUu3dvtw8n3nvvPXXr1k15eXnKyMhQZmamYmNj1b9/fy1evNiqd88992j27Nm6++67tWbNGr3yyiu68cYblZubK+nMKdA33HCDJPefb9SpU6fUce7YsUPjxo3T+PHjtWzZMnXs2FF//vOf9dRTT3nVffDBB7Vv3z7NmTNHK1asUK1atTRlyhSNHj1aLVq00NKlSzVz5kz9+9//VocOHfTVV19Z265du1adOnXSvn37NH36dK1evVoPP/ywvv/+e6vOrl271LZtW+3cuVNPP/20Vq5cqb59++ruu+92Oy3+ySefVHp6uoYMGaJVq1Zp8eLFGj16tNvPAIYNG6bly5fr0Ucf1T//+U+99NJL6tGjhzVnJVm1apVmzZqlxx57TG+++ab14cE333xj1VmzZo0GDhyohIQELV68WE8++aQyMzO1YMGCUtsHAEgyAIByGzFihKlatarPx15++WUjycyZM8cYY0xBQYGJiYkxnTp1CqjtjRs3GklmyZIlfuu0b9/eREVFWffnzZtnJHndIiIizPPPP++2bWFhobn++uutOna73fz2t781Dz30kPnhhx+seosWLXLbj0AcO3bMxMXFmSuvvNIqGzFihLHZbObrr792q+s6h3v27DF2u91MmDDBelySGTNmTIn9PfXUU0aSOXr0qN8627ZtM5LMvHnz3MqLiopMUlKSufzyy01RUZFVXlBQYGrVqmU6duxolU2aNMlIMuPHj3dr49VXXzWSzMKFC40xxrzxxhtGktmxY0eJ4/Z07Ngx43A4zGOPPWaMMebAgQNGkpk4caKJiooyhYWFxhhjbr/9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rvHjxwc0Luffj3YmBb6SB+lMApGfn2996xgeHq6JEydqz549+utf/+pzmx9++EEffvihpLMXQfvf//1fr31/5513FB4eXurVrSdOnKgaNWrogQceCGjfXEVERKhz586aOnWqpDN/L9xZLsnrG9smTZqobt26eu2119w+SDh27JjefPNN60rkrjz/fvnrr7+u06dP+/zQw263q3379taVpT1PV/bUrl07xcXFacaMGcrJybES7R49emj79u16/fXX1bx5c59nMnjOg6/9DZalS5eqsLDQul9QUKAVK1aoU6dOstvtqlq1qtq3b6+lS5e6jaG4uFgLFy5UvXr1dNlll3m1W7t2bY0cOVJDhgzRF198YSWW/vanYcOGbs+fJk2auD3++eef67PPPnMre+211xQbG+t2cThfOnTooKioKC1cuNCt/MCBA3r33XetDzsuu+wypaSkaO7cuTp58qTPtqKjo9W1a1dt375daWlpXs/7Nm3a+PymuVq1arrhhhs0ZswYHTlyxOeV1uvXr6+77rpLPXv2LHV9BaJq1apq06aNli9f7nbxvp9//tnn1ckBAN4u3D8OCQCV1OrVq3Xw4EFNnTrVZ+KVmpqqWbNmKSMjQ/369Su1va+++kpbtmxRcXGxcnNztXXrVmVkZCg/P18vv/yyWrRo4bXNzp07rStu5+bmaunSpVq3bp0GDBigRo0aSZLuuOMOHT16VIMGDVJqaqrsdrv27NmjZ555RmFhYZo4caLV3n333afdu3dr0qRJ+vjjjzV06FAlJycrLy9Pmzdv1osvvqjJkyerffv2evnll9WsWTPrit6e+vfvr7fffluHDx+2kn5PcXFxeuihhwL+EOHRRx/VgQMH1L17d9WrV09Hjx7VzJkzFR4ers6dO0s6czp3VFSUXn31VTVr1kwxMTFKSkpSUlKSnnzySd18883q16+f/vjHP+rkyZOaNm2ajh49qieeeMKrv6VLl6pKlSrq2bOnPv/8cz3yyCNq2bKlbrrpJknSnDlz9O6776pv376qX7++CgsLrQ8XSjvl2263q3PnzlqxYoUaNWpk/S3kq666ShEREdqwYYPuvvvuUufE+bfVZ86cqREjRig8PFxNmjQJ6IOcQNjtdvXs2VP33HOPiouLNXXqVOXn57tdPXvKlCnq2bOnunbtqnvvvVcOh0PPP/+8du7cqczMTOsb5Pbt26tfv35KS0tT9erVtXv3br3yyituH3I492fq1Knq3bu37Ha70tLS3H5D7EtSUpKuvfZapaenq06dOlq4cKHWrVunqVOnen2A4qlatWp65JFH9Je//EXDhw/XkCFDlJubq8mTJysyMlKTJk2y6j733HPq37+/rrzySo0fP17169fXvn37tHbtWuuDmZkzZ+p3v/udOnXqpP/5n/9Rw4YNVVBQoK+//lorVqzQu+++K+nMcyQ1NVVt2rRRYmKivv32W82YMUMNGjTQpZdeqry8PHXt2lVDhw5V06ZNFRsbq23btllXCg+Gxx57TH379lWvXr305z//WUVFRZo2bZpiYmLK/FcBAOCidN4uwwYAvwK+rjp+/fXXG4fD4Xblbk+DBw82VapUMTk5OX7rOK+E7LxVqVLFJCQkmA4dOpi//OUvJjs722sbX1cdj4+PN1dccYWZPn26ddVqY4xZu3atGTVqlGnevLmJj483VapUMXXq1DEDBw40WVlZPsf01ltvmb59+5rExERTpUoVU716ddO1a1czZ84cc/LkSbN8+XIjycyYMcPvfq1Zs8ZIMk8//bTfOTTGmJMnT5pGjRoFdNXxlStXmt69e5u6desah8NhatWqZfr06WPef/99t3qZmZmmadOmJjw83EgykyZNsh5bvny5ad++vYmMjDRVq1Y13bt3Nx9++KHb9s6rjv/rX/8y/fv3NzExMSY2NtYMGTLEfP/991a9rKwsM2DAANOgQQMTERFhEhISTOfOnc3bb79d4n44zZw500gyt99+u1t5z549jSSvdnxdddwYYx588EGTlJRkwsLCjCSzceNGY8yZq3H37dvXq9/OnTubzp07lzg2Z19Tp041kydPNvXq1TMOh8P89re/NWvXrvWq//7775tu3bqZqlWrmqioKHPllVeaFStWuNV54IEHTJs2bUz16tVNRESEady4sRk/frz58ccfrTonT540t912m0lMTDQ2m83vFdVdOffzjTfeMC1atDAOh8M0bNjQTJ8+3a1eaVf4f+mll0xaWppxOBwmPj7eXHfddebzzz/3qpeVlWV69+5t4uPjTUREhElJSfG6Qv3evXvNqFGjTN26dU14eLhJTEw0HTt2NI8//rhV5+mnnzYdO3Y0NWvWNA6Hw9SvX9+MHj3aes4XFhaaO++806SlpZm4uDgTFRVl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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "execution_count": 2,
+ "id": "df942a2c",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:05.816985Z",
+ "iopub.status.busy": "2025-11-24T18:33:05.816985Z",
+ "iopub.status.idle": "2025-11-24T18:33:06.001663Z",
+ "shell.execute_reply": "2025-11-24T18:33:06.001511Z"
}
- ],
+ },
+ "outputs": [],
"source": [
- "fig, ax_barcode = plt.subplots(figsize=(10,1.5))\n",
+ "fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),\n",
+ " gridspec_kw={'height_ratios':[10,1]})\n",
+ "\n",
+ "gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')\n",
+ "city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')\n",
+ "\n",
+ "plot_stops(stops_tadb, ax=ax_map, cmap='Reds')\n",
+ "plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)\n",
+ "ax_map.set_axis_off()\n",
"\n",
"plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops_tadb, ax=ax_barcode, stop_color='red', set_xlim=False, timestamp='unix_ts')\n",
- "plt.title(\"TA-DBSCAN stops with post-processing\")\n",
- "plt.tight_layout()\n",
+ "plot_stops_barcode(stops_tadb, ax=ax_barcode, cmap='Reds', set_xlim=False, timestamp='unix_ts')\n",
+ "\n",
+ "plt.tight_layout(pad=0.1)\n",
"plt.show()"
]
}
@@ -117,7 +119,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.5"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/examples/benchmarking_of_stop_detection_algorithms.ipynb b/examples/benchmarking_of_stop_detection_algorithms.ipynb
index 6097165d..64549ab4 100644
--- a/examples/benchmarking_of_stop_detection_algorithms.ipynb
+++ b/examples/benchmarking_of_stop_detection_algorithms.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
- "id": "92838936",
+ "id": "67fc810a",
"metadata": {},
"source": [
"# Comparing runtimes of different stop detection algorithms on toy datasets"
@@ -10,7 +10,7 @@
},
{
"cell_type": "markdown",
- "id": "cb276fd9",
+ "id": "f152187c",
"metadata": {},
"source": [
"Here we compare the runtimes of four different stop detection algorithms: Lachesis, grid-based, temporal DBSCAN, and HDBSCAN."
@@ -18,28 +18,44 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "19184dee",
- "metadata": {},
+ "execution_count": 1,
+ "id": "474229df",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:19.946986Z",
+ "iopub.status.busy": "2025-11-24T18:33:19.946986Z",
+ "iopub.status.idle": "2025-11-24T18:33:23.251955Z",
+ "shell.execute_reply": "2025-11-24T18:33:23.251955Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "Lachesis execution time: 0.06618499755859375 seconds\n",
- "TA-DBSCAN execution time: 0.13709473609924316 seconds\n",
- "TA-DBSCAN pre-processing time: 0.06432580947875977 seconds\n",
- "TA-DBSCAN clustering time: 0.11364507675170898 seconds\n",
- "TA-DBSCAN post-processing time: 0.02340412139892578 seconds\n",
- "Grid-Based execution time: 0.16486215591430664 seconds\n",
- "HDBSCAN execution time: 2.95216703414917 seconds\n",
- "HDBSCAN clustering time: 2.930790901184082 seconds\n",
- "HDBSCAN post-processing time: 0.02130913734436035 seconds\n"
+ "Lachesis execution time: 0.02721381187438965 seconds\n",
+ "TA-DBSCAN execution time: 0.012791156768798828 seconds\n",
+ "TA-DBSCAN clustering time: 0.009821414947509766 seconds\n",
+ "TA-DBSCAN post-processing time: 0.0029697418212890625 seconds\n",
+ "Grid-Based execution time: 0.022524595260620117 seconds\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "HDBSCAN execution time: 0.3206779956817627 seconds\n",
+ "HDBSCAN clustering time: 0.3206779956817627 seconds\n",
+ "HDBSCAN post-processing time: 0.0 seconds\n"
]
}
],
"source": [
"%matplotlib inline\n",
+ "import matplotlib\n",
+ "matplotlib.use('TkAgg')\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.ion()\n",
"\n",
"# Imports\n",
"import nomad.io.base as loader\n",
@@ -47,9 +63,7 @@
"from shapely.geometry import box\n",
"import pandas as pd\n",
"import numpy as np\n",
- "import matplotlib.pyplot as plt\n",
- "import shapely.plotting as shp_plt\n",
- "from nomad.stop_detection.viz import adjust_zoom, plot_stops_barcode, plot_pings, plot_stops, plot_time_barcode\n",
+ "from nomad.stop_detection.viz import plot_stops_barcode, plot_pings, plot_stops, plot_time_barcode\n",
"import nomad.stop_detection.dbscan as DBSCAN\n",
"import nomad.stop_detection.lachesis as LACHESIS\n",
"import nomad.stop_detection.grid_based as GRID_BASED\n",
@@ -60,10 +74,13 @@
"from tqdm import tqdm\n",
"\n",
"# Load data\n",
- "city = gpd.read_file(\"garden_city.geojson\").to_crs('EPSG:3857')\n",
+ "import nomad.data as data_folder\n",
+ "from pathlib import Path\n",
+ "data_dir = Path(data_folder.__file__).parent\n",
+ "city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')\n",
"outer_box = box(*city.total_bounds).buffer(15, join_style='mitre')\n",
"\n",
- "filepath_root = '../tutorials/IC2S2-2025/gc_data_long/'\n",
+ "filepath_root = 'gc_data_long/'\n",
"tc = {\n",
" \"user_id\": \"gc_identifier\",\n",
" \"timestamp\": \"unix_ts\",\n",
@@ -73,7 +90,7 @@
" \"date\":\"date\"}\n",
"\n",
"users = ['admiring_brattain']\n",
- "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','==', '2024-01-01'), traj_cols=tc)\n",
+ "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)\n",
"\n",
"# Lachesis (sequential stop detection)\n",
"start_time = time.time()\n",
@@ -82,30 +99,21 @@
"print(f\"Lachesis execution time: {execution_time_lachesis} seconds\")\n",
"\n",
"# Density based stop detection (Temporal DBSCAN)\n",
- "start_time_pre_tadbscan = time.time()\n",
- "users = ['confident_aryabhata']\n",
- "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','<=', '2024-01-03'), traj_cols=tc)\n",
- "traj[['longitude','latitude']] = np.column_stack(\n",
- " filters.to_projection(traj, x='dev_x', y='dev_y', data_crs='EPSG:3857', crs_to='EPSG:4326')\n",
- ")\n",
- "time_pre_tadbscan = time.time() - start_time_pre_tadbscan\n",
- "\n",
"start_time = time.time()\n",
"user_data_tadb = traj.assign(cluster=DBSCAN.ta_dbscan_labels(traj, time_thresh=240, dist_thresh=15, min_pts=3, traj_cols=tc))\n",
"clustering_time_tadbscan = time.time() - start_time\n",
"start_time_post = time.time()\n",
- "stops_tadb = post.remove_overlaps(user_data_tadb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)\n",
+ "cluster_labels_tadb = post.remove_overlaps(user_data_tadb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)\n",
"execution_time_tadbscan = time.time() - start_time\n",
"post_time_tadbscan = time.time() - start_time_post\n",
"print(f\"TA-DBSCAN execution time: {execution_time_tadbscan} seconds\")\n",
- "print(f\"TA-DBSCAN pre-processing time: {time_pre_tadbscan} seconds\")\n",
"print(f\"TA-DBSCAN clustering time: {clustering_time_tadbscan} seconds\")\n",
"print(f\"TA-DBSCAN post-processing time: {post_time_tadbscan} seconds\")\n",
"\n",
"# Grid-based\n",
"start_time = time.time()\n",
- "traj['h3_cell'] = filters.to_tessellation(traj, index=\"h3\", res=10, x='dev_x', y='dev_y', data_crs='EPSG:3857')\n",
- "stops_gb = GRID_BASED.grid_based(traj, time_thresh=240, complete_output=True, timestamp='unix_ts', location_id='h3_cell')\n",
+ "traj['h3_cell'] = filters.to_tessellation(traj, index=\"h3\", res=10, traj_cols=tc, data_crs='EPSG:3857')\n",
+ "stops_gb = GRID_BASED.grid_based(traj, time_thresh=240, complete_output=True, traj_cols=tc, location_id='h3_cell')\n",
"execution_time_grid = time.time() - start_time\n",
"print(f\"Grid-Based execution time: {execution_time_grid} seconds\")\n",
"\n",
@@ -114,7 +122,7 @@
"user_data_hdb = traj.assign(cluster=HDBSCAN.hdbscan_labels(traj, time_thresh=240, min_pts=3, min_cluster_size=2, traj_cols=tc))\n",
"clustering_time_hdbscan = time.time() - start_time\n",
"start_time_post = time.time()\n",
- "stops_hdb = post.remove_overlaps(user_data_hdb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3) \n",
+ "cluster_labels_hdb = post.remove_overlaps(user_data_hdb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3) \n",
"execution_time_hdbscan = time.time() - start_time\n",
"post_time_hdbscan = time.time() - start_time_post\n",
"print(f\"HDBSCAN execution time: {execution_time_hdbscan} seconds\")\n",
@@ -124,7 +132,7 @@
},
{
"cell_type": "markdown",
- "id": "5b150f1e",
+ "id": "c88f426d",
"metadata": {},
"source": [
"## Summary of Single-User Performance"
@@ -132,7 +140,7 @@
},
{
"cell_type": "markdown",
- "id": "07203969",
+ "id": "6a678431",
"metadata": {},
"source": [
"### Lachesis"
@@ -140,177 +148,57 @@
},
{
"cell_type": "code",
- "execution_count": 7,
- "id": "570b6103",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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Nh7lA0WLcw9wxkQ/9whpE0QymdTnPvc5XX311YddTPbGG2/UO9/5TknS61//evX29mrVqlX7Pc7n8+05mCXppptu0jvf+U75fL78PwGgSHHuAc6YDOceIW0CyGQyeutb3yrbtvWTn/zkkI9du3atnn76aV199dWSJK/Xq6uuuko33njjAR9/zTXX6E9/+pN27typP/3pT7rmmmvyXj9QrDj3AGdMlnPP61jLyIuhA3XLli16+OGHR/TbRDab1dSpU/d8zbZtBQIB/ehHP1Jpaelejz/qqKPU0NCgq6++WosWLdLSpUu1Zs2a8XgqQFHh3AOcMZnOPXrSitjQgbp+/Xo9+OCDqqysPOTjs9msfvOb3+i73/2u1qxZs+fPiy++qKlTp+qWW2454HbXXHONHn30UX6TB3bj3AOcMdnOPXrSXKq/v18bNmzY8//NmzdrzZo1qqio0IwZM5TJZPSWt7xFzz//vO6++27lcjnt3LlTklRRUSG/37/fPu+++251d3fr2muv3e83hze/+c268cYb9aEPfWi/7T7wgQ/oyiuvPOzATGAi4NwDnMG5dwAFvZcUI/bII4/Ykvb78573vMe27X/dLnygP4888sgB93nppZfaF1988QG/99RTT9mS7BdffHHPvl944YUDPvaFF15gGgBMWJx7gDM49/Zn2PYB7kEFAACAoxiTBgAA4EKENAAAABcipAEAALgQIQ0AAMCFCGkAAAAuREgDAABwIUIaAACACxHSAAAAXIiQBgAA4EKENAAAABcipAEAALgQIQ0AAMCF/j/YVMXrVVyP4QAAAABJRU5ErkJggg==",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "execution_count": 2,
+ "id": "b7480c93",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:23.251955Z",
+ "iopub.status.busy": "2025-11-24T18:33:23.251955Z",
+ "iopub.status.idle": "2025-11-24T18:33:23.475346Z",
+ "shell.execute_reply": "2025-11-24T18:33:23.475346Z"
}
- ],
+ },
+ "outputs": [],
"source": [
"fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),\n",
" gridspec_kw={'height_ratios':[10,1]})\n",
"\n",
- "shp_plt.plot_polygon(outer_box, ax=ax_map, add_points=False, color='#0e0e0e')\n",
- "city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#2c353c')\n",
+ "gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')\n",
+ "city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')\n",
"\n",
- "plot_stops(stops, ax=ax_map, cmap='Reds', x='x', y='y')\n",
+ "plot_stops(stops, ax=ax_map, cmap='Reds')\n",
"plot_pings(traj, ax=ax_map, s=6, point_color='black', cmap='twilight', traj_cols=tc)\n",
- "\n",
- "adjust_zoom(stops['x'], stops['y'], buffer=1.4, ax=ax_map)\n",
"ax_map.set_axis_off()\n",
"\n",
"plot_time_barcode(traj[tc['timestamp']], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops, ax=ax_barcode, cmap='Reds', set_xlim=False, x='x', y='y', timestamp='unix_ts')\n",
+ "plot_stops_barcode(stops, ax=ax_barcode, cmap='Reds', set_xlim=False, timestamp='unix_ts')\n",
"\n",
"plt.tight_layout(pad=0.1)\n",
"plt.show()"
]
},
- {
- "cell_type": "markdown",
- "id": "d286bd02",
- "metadata": {},
- "source": [
- "### TADBSCAN"
- ]
- },
{
"cell_type": "code",
- "execution_count": 20,
- "id": "2159107b",
- "metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "/Users/carolinechen/Desktop/cs/nomad/nomad/io/base.py:104: UserWarning: Trajectory column 'unix_ts' specified for 'timestamp' not found in DataFrame.\n",
- " warnings.warn(f\"Trajectory column '{value}' specified for '{key}' not found in DataFrame.\")\n"
- ]
- },
- {
- "data": {
- "image/png": "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a4eFhenNN9/U3//+d9WsWVPdunXTW2+9pVOnTpXYl65du+rkyZNKS0vTN998I4fDoc6dO3sk4ykpKerSpYtPf0ujfv36PmVVqlQJ+EOCksbANWbNmjXzqdeiRQudPXtWly5dknR9685737l8+XKx/YyKilLt2rV9zq1v1KiRT9sZGRlF9t99GQ8dOqRmzZp5XC/B28GDB5WZmanY2FjVqFHD43Hx4kVrX+vevbuGDBmiV199VdWrV9egQYN8bg/49NNPq2nTpurbt6/i4uKsD88CERcX53N3AO/1npGRocaNG/vUK+1dFwAAhUi6AcAmGzdu1MmTJ7V06VI1adLEegwfPlySrG98v/nmG9WuXdvjcfTo0YDm4bpwVSAHw3FxcWrWrJm2bNnid3rFihXVtWtXzZkzRy+//LLOnz+vv//975Kk5s2bS5J+/PHHgPqVnJysK1euaNasWR7L/txzz0lSQN927969W3/9618Dmp8kDR8+XIcPH9Z7772nOnXqaObMmWrVqpW1DDfaM888owMHDuhPf/qTwsPDNX36dLVo0UK7du0q9nWub1W3bNmilJQU3XvvvYqMjLSS7osXL2rXrl0e31Jfi6CgIL/lpfmgo6xcz7rz3nc+/fTTa+qD3VcqLygoUGxsrPULFO/Ha6+9Jqnwm/Fly5bp22+/1eTJk3X8+HGNGzdO7du3tz5ki42N1e7du7V69WoNHDhQmzZtUt++fYu8M4C78rTeAeBOQdINADZJTExUbGyskpOTfR4JCQlauXKlLl++rLZt2/ocgPu70Jg/rnuDe/8stij5+fnWgXtx7rvvPkmFPxGWpPj4eEnSJ598EtB8EhMT1bp1a7/L/vDDD2vJkiXFvn7kyJG6++679eqrr5YqGahdu7aefvpprVq1Sunp6apWrZreeOMNa3pR9/9u0KCBDh48qIKCAo/y/fv3W9PdHTx40KeNAwcO+FwgrXHjxnr++ef1xRdfaO/evcrNzdWsWbOKXYb69eurfv36SklJUUpKipVcd+vWTUeOHFFycrKcTqe6detWbDt23evcpaQxcI1ZamqqT739+/erevXqioyMtMqudd157zve+4J3Py9evKiTJ08GdO/yBg0aFNl/92Vs3LixUlNTi/w5uKvOuXPn1KVLF+tXKO6Ptm3betS///779cYbb2jHjh1KTEzUTz/9pKVLl1rTQ0NDFR8fr7lz5+rQoUOaOHGiFi9erLS0tBKXK5DlPnTokM++VxZtA8CdiKQbAGxw+fJlrVixQgMGDNDQoUN9HpMnT1ZWVpZWr16tKlWq+ByAB3J/3SVLlujDDz9U586d1atXrxLrHzhwQKmpqR4H966fuHtzne/q+mlt586d9eijj+rDDz/0e651bm6u/umf/kmSdPToUW3ZskXDhw/3u+xjx45VWlqatm3bVmRf3b/tdl05vThOp1OZmZkeZbGxsapTp47Hz3IjIyN96klSv379dOrUKY9vSfPz8/Xee+8pKipK3bt396i/atUqHT9+3Hq+fft2bdu2zbr6c3Z2tnJycjxe07hxY1WqVMmjP0Xp2rWrNm7cqO3bt1tJd7t27VSpUiX9+7//uyIiItS+ffti23AltL/++muJ87sWJY1B7dq11a5dOy1atMijD3v37tUXX3yhfv36Sbr+dee979SuXdtj+vz58z2S4Xnz5ik/P9/qZ3H69eun7du369tvv7XKLl26pPnz56thw4Zq2bKlpMIr1Z89e1Zz5szxacOVuA4fPlxOp1Ovv/66T538/HxrjM6fP++T7LZr106SrPE4d+6cx/QKFSpYV7sPZPsqSZ8+fXT8+HGPfS8nJ0cffPDBdbcNAHcibhkGADZYvXq1srKyPC5E5O7+++9XjRo1lJiY6HGv5aIsW7ZMUVFRys3N1fHjx7Vu3Tp9/fXXatu2rZKTk33q5+fnW99KFxQU6MiRI/rzn/+sgoICvfLKK1a9QYMGqVGjRoqPj1fjxo116dIlbdiwQX/729/UoUMH6xtuqfAWUb1799bgwYMVHx+vXr16KTIyUgcPHtTSpUt18uRJvf3221qyZImMMUUue79+/RQcHKzExER16tSpyGV+8skn9frrr2v37t0ljk9WVpbi4uI0dOhQtW3bVlFRUdqwYYO+//57j2+W27dvr08//VTPPfecOnTooKioKMXHx2vChAl6//33NWbMGP3www9q2LChli1bpq+//lqzZ8/2OWf+7rvv1oMPPqg//OEPunLlimbPnq1q1arphRdekFT4AUevXr00fPhwtWzZUsHBwVq5cqVOnz6tESNGlLg8Xbt2VWJiohwOh/Vz86CgID3wwANat26devTo4ffe6u7atWunoKAgvfnmm8rMzFRYWJgeeughj/uvX4+SxkCSZs6cqb59+6pz584aP368dcuwypUra8aMGZKuf92VJDc311oXqampmjt3rh588MEit093f/zjH5WUlKS+fftq6tSpqlq1qhYtWqT09HQtX77cOqd+1KhRWrx4sZ577jnrgxLXvvT0009r0KBB6t69uyZOnKg//elP2r17t3r37q2QkBAdPHhQycnJevfddzV06FAtWrRIc+fO1WOPPabGjRsrKytLH3zwgaKjo60PKn73u9/p//7v//TQQw8pLi5OGRkZeu+999SuXTuP28Jdq4kTJ2rOnDlKSEjQtGnTVLt2bSUmJlofBtr9KwoAuO3cvAunA8Dtwd8tw+Lj4014eLi5dOlSka8bM2aMCQkJMWfPni2yjut2Wa5HeHi4iYuLMwMGDDALFy40OTk5Pq/xd8uw6Oho06tXL7NhwwaPuklJSWbEiBGmcePGJiIiwoSHh5uWLVual156ybpNlrvs7Gzz9ttvmw4dOpioqCgTGhpqmjRpYqZMmWLS0tKMMca0adPG1K9fv9gx69Gjh4mNjTV5eXketwzz5rrtk0q4ZdiVK1fMP//zP5u2bduaSpUqmcjISNO2bVszd+5cj3oXL140TzzxhImJiTGSPG5Bdfr0aTN27FhTvXp1Exoaatq0aWM++ugjj9e793XWrFmmXr16JiwszHTt2tXj9l1nz541kyZNMs2bNzeRkZGmcuXKplOnTuYvf/lLsePi8tNPPxlJpkWLFh7l//Zv/2YkmenTp/u8xvuWYcYY88EHH5i77rrLBAUFedzqqUGDBqZ///4+bXTv3t1079692L4FOgYuGzZsMF26dDEREREmOjraxMfHm3379lnTy2Ld+ePadr766iszYcIEU6VKFRMVFWWefPJJj1uYFTcexhhz6NAhM3ToUBMTE2PCw8NNx44dzZo1a3zqZWdnm5deesk0atTIhISEmFq1apmhQ4eaQ4cOedSbP3++ad++vYmIiDCVKlUybdq0MS+88II5ceKEMcaYnTt3moSEBFO/fn0TFhZmYmNjzYABA8yOHTusNpYtW2Z69+5tYmNjTWhoqKlfv76ZOHGiOXnypFWnqFuGtWrVyqfvo0eP9hnPw4cPm/79+5uIiAhTo0YN8/zzz5vly5cbSea7777zP+gAAL8cxnDlDAAAAnHkyBE1atRIM2fOtH5Of6e5Vcbg448/1tixY/X9999b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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "fig, ax_barcode = plt.subplots(figsize=(10,1.5))\n",
- "\n",
- "plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops_tadb, ax=ax_barcode, stop_color='red', set_xlim=False, timestamp='unix_ts')\n",
- "plt.title(\"TA-DBSCAN stops with post-processing\")\n",
- "plt.tight_layout()\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "9c2b278a",
- "metadata": {},
- "source": [
- "### Grid-Based"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 21,
- "id": "62555a1b",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAACMCAYAAABh9MpJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAf0UlEQVR4nO3deXRU9f3/8dckk8m+sCSWyFYwrCL8vojSooKagobFVhFMwUKpHkQE7TmtonXDys+D39qjglCkgFplEUQPxpZzsA0uHJUlRI9VItqEqoAQCFkJ2T6/P/jd68xkJpkslwzx+ThnDsy9n/v5vO/nfu7n3ndmcxljjAAAAAAAQLuL6OgAAAAAAADorEi6AQAAAABwCEk3AAAAAAAOIekGAAAAAMAhJN0AAAAAADiEpBsAAAAAAIeQdAMAAAAA4BCSbgAAAAAAHELSDQAAAACAQ0i6AQA/GLNnz1bfvn2bLVdUVCSXy6UXXnjB8Zg6St++fTV79uyODgMAgE6PpBsAEPYKCwt11113acCAAYqLi1NcXJyGDBmi+fPn65NPPumwuHbu3CmXy+Xz6Nq1q0aPHq1XXnmlw+I6lz777DM9+uijKioq6uhQAAAIS+6ODgAAgKbk5ORo+vTpcrvdmjFjhoYPH66IiAgdOHBAW7du1cqVK1VYWKg+ffo0W9fq1avV0NDQ7jEuXLhQo0aNkiSdOHFCmzZt0syZM3Xq1CnNnz+/3dsLJ5999pkWL16scePGhfQuAgAAfmhIugEAYeurr77SLbfcoj59+uif//ynevTo4bN+6dKlWrFihSIimn7jVmVlpeLj4xUVFeVInFdeeaWmTp1qP583b5769eun9evXd/qkGwAANI23lwMAwtaTTz6pyspKrVu3rlHCLUlut1sLFy5Ur1697GWzZ89WQkKCvvrqK2VlZSkxMVEzZsyw1/m/Gnvq1CnNnj1bycnJSklJ0axZs3Tq1Kk2xe3xeNSlSxe53b5/2163bp2uueYapaWlKTo6WkOGDNHKlSsbbb93715NmDBB3bt3V2xsrH784x9rzpw5PmUaGhr09NNPa+jQoYqJidEFF1yguXPnqqSkxKecMUaPP/64evbsqbi4OF199dX697//HfK+bNy4USNHjlRiYqKSkpI0bNgwPfPMM5KkF154QTfffLMk6eqrr7bfYr9z5057+xUrVmjo0KGKjo5Wenq65s+f36h/x40bp4svvlj79u3TT3/6U3uf//KXvzSKZ9myZRo6dKji4uLUpUsXXXrppVq/fn3I+wMAwLnGK90AgLCVk5Ojiy66SJdffnmLtqurq9OECRN0xRVX6E9/+pPi4uICljPG6IYbbtD777+vO+64Q4MHD9brr7+uWbNmtai98vJyFRcXS5JOnjyp9evX69NPP9WaNWt8yq1cuVJDhw7VlClT5Ha79eabb+rOO+9UQ0OD/Yr4sWPHNH78eKWmpmrRokVKSUlRUVGRtm7d6lPX3Llz9cILL+jXv/61Fi5cqMLCQi1fvlz79+/Xrl277Ff1H374YT3++OPKyspSVlaW8vLyNH78eNXU1DS7Xzt27FB2drauvfZaLV26VJL0+eefa9euXbr77rt11VVXaeHChXr22Wf1wAMPaPDgwZJk//voo49q8eLFyszM1Lx581RQUKCVK1dqz549PjFKUklJibKysjRt2jRlZ2fr1Vdf1bx58+TxeOw/OKxevVoLFy7U1KlTdffdd6u6ulqffPKJPvroI/3yl79s0TEDAOCcMQAAhKHS0lIjyfz85z9vtK6kpMQcP37cflRVVdnrZs2aZSSZRYsWNdpu1qxZpk+fPvbzN954w0gyTz75pL2srq7OXHnllUaSWbduXZMx5ubmGkmNHhEREWbJkiWNynvHaZkwYYLp16+f/fz11183ksyePXuCtvvee+8ZSeaVV17xWb59+3af5ceOHTMej8dMnDjRNDQ02OUeeOABI8nMmjWryf27++67TVJSkqmrqwtaZvPmzUaSyc3N9VlutT1+/HhTX19vL1++fLmRZNauXWsvGzt2rJFknnrqKXvZmTNnzIgRI0xaWpqpqakxxhhzww03mKFDhzYZMwAA4Ya3lwMAwlJZWZkkKSEhodG6cePGKTU11X4899xzjcrMmzev2Tb+/ve/y+12+5SNjIzUggULWhTrww8/rB07dmjHjh3atGmTsrOz9Yc//MF+G7YlNjbW/n9paamKi4s1duxY/ec//1FpaakkKSUlRdLZV/lra2sDtrd582YlJyfrZz/7mYqLi+3HyJEjlZCQoNzcXEnS22+/rZqaGi1YsEAul8ve/p577glpv1JSUlRZWakdO3aE2hU2q+177rnH5zP3t99+u5KSkvTWW2/5lHe73Zo7d6793OPxaO7cuTp27Jj27dtnx/PNN99oz549LY4HAICOQtINAAhLiYmJkqSKiopG61atWqUdO3bo5ZdfDrit2+1Wz549m23j0KFD6tGjR6PEfuDAgT7PT58+raNHj/o8vA0bNkyZmZnKzMzUtGnT9PLLL2vSpElatGiRjh8/bpfbtWuXMjMzFR8fr5SUFKWmpuqBBx6QJDvpHjt2rG666SYtXrxY3bt31w033KB169bpzJkzdj0HDx5UaWmp0tLSfP74kJqaqoqKCh07dszeP0nKyMjwiTc1NVVdunRptn/uvPNODRgwQNdff7169uypOXPmaPv27c1u5922f196PB7169fPXm9JT09XfHy8z7IBAwZIkv1zZPfdd58SEhJ02WWXKSMjQ/Pnz9euXbtCigcAgI5C0g0ACEvJycnq0aOHPv3000brLr/8cmVmZmrMmDEBt42Ojm72G81bYtOmTerRo4fPoznXXnutqqurtXv3bklnv4n92muvVXFxsf785z/rrbfe0o4dO/Tb3/5WkuyfMnO5XNqyZYs++OAD3XXXXfr22281Z84cjRw50v4DRENDg9LS0uxX1/0fjz32WLvsd1pamvLz87Vt2zZNmTJFubm5uv7661v8mff2MnjwYBUUFGjjxo264oor9Nprr+mKK67QI4880iHxAAAQCr5IDQAQtiZOnKi//vWv2r17ty677LJ2r9/6KbKKigqfV7sLCgp8yk2YMKHFb7Guq6uT9P0r9W+++abOnDmjbdu2qXfv3nY5663g/kaPHq3Ro0dryZIlWr9+vWbMmKGNGzfqtttuU//+/fX2229rzJgxPm9ZD7R/0tlXxvv162cvP378eKNvOQ/G4/Fo8uTJmjx5shoaGnTnnXdq1apVeuihh3TRRRf5vG09UNsFBQU+bdfU1KiwsFCZmZk+5Q8fPmz/tJvliy++kCSfb5yPj4/X9OnTNX36dNXU1OjGG2/UkiVLdP/99ysmJiakfQIA4FzilW4AQNi69957FRcXpzlz5ui7775rtN4Y06b6s7KyVFdX5/OzXfX19Vq2bJlPuR49ethvH7cezcnJyZEkDR8+XNLZz4r7x1xaWqp169b5bFdSUtJov0aMGCFJ9lvMp02bpvr6ev3xj39s1G5dXZ39k1yZmZmKiorSsmXLfOp8+umnm41fkk6cOOHzPCIiQpdccolPLFaS7P8zYJmZ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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "fig, ax_barcode = plt.subplots(figsize=(10,1.5))\n",
- "\n",
- "plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops_gb, ax=ax_barcode, stop_color='green', set_xlim=False, timestamp='unix_ts')\n",
- "plt.title(\"Grid-Based stops\")\n",
- "plt.tight_layout()\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "779c39db",
- "metadata": {},
- "source": [
- "### HDBSCAN"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 22,
- "id": "fa70719e",
- "metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "/Users/carolinechen/Desktop/cs/nomad/nomad/io/base.py:104: UserWarning: Trajectory column 'unix_ts' specified for 'timestamp' not found in DataFrame.\n",
- " warnings.warn(f\"Trajectory column '{value}' specified for '{key}' not found in DataFrame.\")\n"
- ]
- },
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAACYCAYAAAD5s4rEAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAnYklEQVR4nO3deXwV5b3H8e/JSggJYQlLIILsS9ikrmFr1VJkq4JAKIKA4ALc1qr1VmuBVqrgdaHghhctXiFCVIpCvb1yAQEFBBEU2QQJVZAQIIRAEpKcPPeP3BnOluQQMiTEz/v1Oq/kzDzzrHOemd9ZZlzGGCMAAAAAAFDpQqq6AgAAAAAA1FQE3QAAAAAAOISgGwAAAAAAhxB0AwAAAADgEIJuAAAAAAAcQtANAAAAAIBDCLoBAAAAAHAIQTcAAAAAAA4h6AYAAAAAwCEE3QAAXAHWrVsnl8uldevWBZ32nXfecb5iqDJ33323WrZsWdXVAACUg6AbACrR3/72N7lcLm3bti3g+n79+ikpKclrWcuWLeVyueRyuRQSEqK4uDh16dJFkydP1pYtWwLmY6W3HtHR0erUqZOefPJJ5ebmeqUtKCjQ3Llz1aNHD8XGxiouLk6dO3fW5MmTtXfvXr+8Dx48qHvvvVetWrVSrVq1FBsbq+TkZM2dO1d5eXl+6d1utxISEuRyufThhx8GrO+MGTPkcrnUuHFjv/pZfTBo0KCA216M3NxczZgxI6jAtCZYsmSJXnjhhaquRqX4sY0dAODHI6yqKwAAkLp3766HHnpIkpSTk6M9e/YoLS1Nr732mh588EE999xzftvceuutGjt2rCTp7Nmz2rBhg5544gnt3LlTaWlpdrphw4bpww8/VEpKiiZNmqTCwkLt3btXK1eu1E033aQOHTrYaVetWqU777xTkZGRGjt2rJKSklRQUKCNGzfqkUce0ddff60FCxZ41WPNmjX64Ycf1LJlSy1evFgDBgwotZ3Hjx/Xyy+/bLe1suXm5mrmzJmSSt7gqEn69OmjvLw8RURE2MuWLFmiXbt26Te/+U3VVayS1OSxc8prr72m4uLiqq4GAKAcBN0AUA00a9ZMY8aM8Vo2e/ZsjR49Ws8//7zatm2r+++/32t9u3btvLa57777VFBQoPfee0/5+fmqVauWtm7dqpUrV2rWrFl67LHHvLafP3++Tp8+bT8/dOiQRo0apRYtWmjNmjVq2rSpvW7KlCk6cOCAVq1a5Vf3t956S9dcc43GjRunxx57TOfOnVN0dHTAdnbv3l3PPPOMHnjgAUVFRQXdP5BCQkJUq1atqq5GjZGfn6+IiAiFhFy5X/oLDw+v6ioAAIJw5R5pAKCGi4qK0n/913+pfv36mjVrlowx5W7TpEkTuVwuhYWVvKd68OBBSVJycrJf2tDQUDVo0MB+PmfOHJ09e1YLFy70Crgtbdq00a9//WuvZXl5eVq+fLlGjRqlESNGKC8vTytWrCi1fn/84x+VkZGhl19+udy2BLJt2zb1799fDRs2VFRUlK6++mpNmDBBkpSenq74+HhJ0syZM+2v3s+YMcPefs2aNerdu7eio6MVFxenoUOHas+ePV5lWF+F37t3r0aMGKHY2Fg1aNBAv/71r5Wfn++V9qOPPlKvXr0UFxenOnXqqH379n5vbvi64447dM0113gtGzx4sFwul95//3172ZYtW7y+su/7m+5+/fpp1apVOnz4sN1W39/3FhcXa9asWWrevLlq1aqlm2++WQcOHCi7ky+yD4qKivTnP/9ZrVu3VmRkpFq2bKnHHntM58+f90p3qWMXiPVzjc8//1w33XSTne8rr7zilc7qu7ffflt/+MMf1KxZM9WuXVtnzpyRJKWlpalnz56KiopSw4YNNWbMGB05csSvPKs/4uPjFRUVpfbt2+vxxx/3SnPkyBFNmDBBjRs3VmRkpDp37qzXX3/dL6958+apc+fOql27turVq6ef/OQnWrJkib0+JydHv/nNb9SyZUtFRkaqUaNGuvXWW7V9+3Y7je9vutPT0+VyufQf//EfWrBggT0m1157rbZu3epXh7S0NHXq1Em1atVSUlKSli9fzu/EAcABfNINAA7Izs7WiRMn/JYXFhZeVD516tTR7bffroULF2r37t3q3LmzvS4/P98u49y5c/rkk0+0aNEijR492g66W7RoIUlavHixkpOT7eWBfPDBB2rVqpVuuummoOv3/vvv6+zZsxo1apSaNGmifv36afHixRo9enTA9L1799bPfvYzzZkzR/fff/9Ffdp9/Phx/fznP1d8fLz+/d//XXFxcUpPT9d7770nSYqPj9fLL7+s+++/X7fffrvuuOMOSVLXrl0lSatXr9aAAQPUqlUrzZgxQ3l5eZo3b56Sk5O1fft2v0BjxIgRatmypZ566ilt3rxZf/3rX5WVlaU333xTkvT1119r0KBB6tq1q/70pz8pMjJSBw4c0CeffFJmO3r37q0VK1bozJkzio2NlTFGn3zyiUJCQrRhwwYNGTJEkrRhwwaFhIQEfMNEkh5//HFlZ2fr+++/1/PPPy+pZH/x9PTTTyskJEQPP/ywsrOzNWfOHP3qV78q9VoBvsrrA0m65557tGjRIg0fPlwPPfSQtmzZoqeeekp79uzR8uXLJV362JUlKytLt912m0aMGKGUlBQtW7ZM999/vyIiIuyg3vLnP/9ZERERevjhh3X+/HlFRETob3/7m8aPH69rr71WTz31lDIyMjR37lx98skn+uKLLxQXFydJ+vLLL9W7d2+Fh4dr8uTJatmypQ4ePKgPPvhAs2bNkiRlZGTohhtukMvl0tSpUxUfH68PP/xQEydO1JkzZ+yfAbz22mv6t3/7Nw0fPtx+I+PLL7/Uli1b7NfOfffdp3feeUdTp05Vp06ddPLkSW3cuFF79uzxe9PG15IlS5STk6N7771XLpdLc+bM0R133KFvv/3W/nR81apVGjlypLp06aKnnnpKWVlZmjhxopo1a1ZunwMALpIBAFSaN954w0gq89G5c2evbVq0aGEGDhxYap7PP/+8kWRWrFhhLyst71/+8pcmPz/fTldcXGz69u1rJJnGjRublJQU8+KLL5rDhw97lZGdnW0kmaFDh15UewcNGmSSk5Pt5wsWLDBhYWHm+PHjXummT59uJJnMzEzz8ccfG0nmueeeC7oPjDFm+fLlRpLZunVrqWkyMzONJDN9+nS/dd27dzeNGjUyJ0+etJft3LnThISEmLFjx/rVdciQIV7bP/DAA0aS2blzpzHmwrhkZmaWWW9fW7duNZLMP/7xD2OMMV9++aWRZO68805z/fXX2+mGDBlievToYT9fu3atkWTWrl1rLxs4cKBp0aKFXxlW2o4dO5rz58/by+fOnWskma+++qrMOgbbBzt27DCSzD333OOV7uGHHzaSzJo1a4wxlz52pbH27WeffdZedv78eXusCwoKjDEX+qNVq1YmNzfXTltQUGAaNWpkkpKSTF5enr185cqVRpL54x//aC/r06ePiYmJ8XvtFBcX2/9PnDjRNG3a1Jw4ccIrzahRo0zdunXtsocOHeo3D/iqW7eumTJlSplpxo0b5zX+hw4dMpJMgwYNzKlTp+zlK1asMJLMBx98YC/r0qWLad68ucnJybGXrVu3zkgKuE8BACqOr5cDgANefPFFffTRR36PYD6582V9epmTk+O1fOjQoXa+K1as0O9//3v993//t0aPHm1/Fd3lcumf//ynnnzySdWrV0+pqamaMmWKWrRooZEjR9q/6ba+ZhsTExN0vU6ePKl//vOfSklJsZcNGzZMLpdLy5YtK3W7Pn366Kc//anmzJkT8GropbE+cVy5cuVFf2Pghx9+0I4dO3T33Xerfv369vKuXbvq1ltv1T/+8Q+/baZMmeL1fNq0aZJkp7Xqs2LFiou6mFWPHj1Up04drV+/XlLJJ9rNmzfX2LFjtX37duXm5soYo40bN6p3794X1U5f48eP97rwmpXft99+G9T25fWB9fe3v/2tVzrrQnnWNQAuZezKExYWpnvvvdd+HhERoXvvvVfHjx/X559/7pV23LhxXt+u2LZtm44fP64HHnjA6/fyAwcOVIcOHez6Z2Zmav369ZowYYKuuuoqrzxdLpckyRijd999V4MHD5YxRidOnLAf/fv3V3Z2tv3V8Li4OH3//fcBv/JtiYuL05YtW3T06NGL7pORI0eqXr169nPfcT969Ki++uorjR071uvbEX379lWXLl0uujwAQNkIugHAAdddd51uueUWv4fniXCwzp49K8k/IG7evLmd75AhQ/SXv/xFTz75pN577z2tXLnSThcZGanHH39ce/bs0dGjR5WamqobbrhBy5Yt09SpUyVJsbGxkvwD+7IsXbpUhYWF6tGjhw4cOKADBw7o1KlTuv7667V48eIyt50xY4aOHTvm99vbsvTt21fDhg3TzJkz1bBhQw0dOlRvvPGG32+HAzl8+LAkqX379n7rOnbsqBMnTujcuXNey9u2bev1vHXr1goJCVF6erqkksAmOTlZ99xzjxo3bqxRo0Zp2bJl5QbgoaGhuvHGG7VhwwZJJUF379691atXL7ndbm3evFm7d+/WqVOnLjno9g0Qrf0vKysrqO3L64PDhw8rJCREbdq08UrXpEkTxcXF2f1+KWOXl5enY8eOeT08JSQk+F24r127dpJk19Ny9dVXez0va7/o0KGDvd4KVn1v9+cpMzNTp0+f1oIFCxQfH+/1GD9+vKSSr9lL0qOPPqo6derouuuuU9u2bTVl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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "execution_count": 3,
+ "id": "98cdda1f",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:23.475346Z",
+ "iopub.status.busy": "2025-11-24T18:33:23.475346Z",
+ "iopub.status.idle": "2025-11-24T18:33:23.482904Z",
+ "shell.execute_reply": "2025-11-24T18:33:23.482904Z"
}
- ],
- "source": [
- "fig, ax_barcode = plt.subplots(figsize=(10,1.5))\n",
- "\n",
- "plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops_hdb, ax=ax_barcode, stop_color='blue', set_xlim=False, timestamp='unix_ts')\n",
- "fig.suptitle(\"HDBSCAN stops with post-processing\")\n",
- "plt.tight_layout()\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "id": "c6351f6e",
- "metadata": {},
+ },
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Summary of Single-User Performance\n",
- "Lachesis execution time: 0.06618499755859375 seconds\n",
- "TA-DBSCAN execution time: 0.13709473609924316 seconds\n",
- "Grid-Based execution time: 0.16486215591430664 seconds\n",
- "HDBSCAN execution time: 2.95216703414917 seconds\n"
+ "Lachesis execution time: 0.02721381187438965 seconds\n",
+ "TA-DBSCAN execution time: 0.012791156768798828 seconds\n",
+ "Grid-Based execution time: 0.022524595260620117 seconds\n",
+ "HDBSCAN execution time: 0.3206779956817627 seconds\n"
]
}
],
@@ -324,25 +212,31 @@
},
{
"cell_type": "code",
- "execution_count": 9,
- "id": "d0431a3c",
- "metadata": {},
+ "execution_count": 4,
+ "id": "41e9a154",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:23.482904Z",
+ "iopub.status.busy": "2025-11-24T18:33:23.482904Z",
+ "iopub.status.idle": "2025-11-24T18:33:23.492088Z",
+ "shell.execute_reply": "2025-11-24T18:33:23.492088Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Runtime Disaggregation\n",
- "Lachesis clustering time: 0.06618499755859375 seconds\n",
+ "Lachesis clustering time: 0.02721381187438965 seconds\n",
"--------------------------------\n",
- "TA-DBSCAN pre-processing time: 0.06432580947875977 seconds\n",
- "TA-DBSCAN clustering time: 0.11364507675170898 seconds\n",
- "TA-DBSCAN post-processing time: 0.02340412139892578 seconds\n",
+ "TA-DBSCAN clustering time: 0.009821414947509766 seconds\n",
+ "TA-DBSCAN post-processing time: 0.0029697418212890625 seconds\n",
"--------------------------------\n",
- "Grid-Based clustering time: 0.16486215591430664 seconds\n",
+ "Grid-Based clustering time: 0.022524595260620117 seconds\n",
"--------------------------------\n",
- "HDBSCAN clustering time: 2.930790901184082 seconds\n",
- "HDBSCAN post-processing time: 0.02130913734436035 seconds\n"
+ "HDBSCAN clustering time: 0.3206779956817627 seconds\n",
+ "HDBSCAN post-processing time: 0.0 seconds\n"
]
}
],
@@ -350,7 +244,6 @@
"print(\"Runtime Disaggregation\")\n",
"print(f\"Lachesis clustering time: {execution_time_lachesis} seconds\")\n",
"print(\"--------------------------------\")\n",
- "print(f\"TA-DBSCAN pre-processing time: {time_pre_tadbscan} seconds\")\n",
"print(f\"TA-DBSCAN clustering time: {clustering_time_tadbscan} seconds\")\n",
"print(f\"TA-DBSCAN post-processing time: {post_time_tadbscan} seconds\")\n",
"print(\"--------------------------------\")\n",
@@ -362,7 +255,7 @@
},
{
"cell_type": "markdown",
- "id": "2de448bd",
+ "id": "5c9ee070",
"metadata": {},
"source": [
"## Pings vs Runtime"
@@ -370,36 +263,83 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "dc99ecd5",
- "metadata": {},
+ "execution_count": 5,
+ "id": "62ed6a42",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:23.492088Z",
+ "iopub.status.busy": "2025-11-24T18:33:23.492088Z",
+ "iopub.status.idle": "2025-11-24T18:33:23.520075Z",
+ "shell.execute_reply": "2025-11-24T18:33:23.520075Z"
+ }
+ },
"outputs": [],
"source": [
- "traj = loader.sample_from_file(filepath_root, frac_users=0.1, format='parquet', traj_cols=tc, seed=10) # try frac_users = 0.1\n",
+ "traj = loader.sample_from_file(filepath_root, frac_users=0.1, format='parquet', traj_cols=tc, seed=10)\n",
"\n",
"# H3 cells for grid_based stop detection method\n",
- "traj['h3_cell'] = filters.to_tessellation(traj, index=\"h3\", res=10, x='dev_x', y='dev_y', data_crs='EPSG:3857')\n",
+ "traj['h3_cell'] = filters.to_tessellation(traj, index=\"h3\", res=10, traj_cols=tc, data_crs='EPSG:3857')\n",
"pings_per_user = traj['gc_identifier'].value_counts()"
]
},
{
"cell_type": "code",
- "execution_count": 12,
- "id": "4609ebe7",
- "metadata": {},
+ "execution_count": 6,
+ "id": "baafc0b8",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:23.520075Z",
+ "iopub.status.busy": "2025-11-24T18:33:23.520075Z",
+ "iopub.status.idle": "2025-11-24T18:33:24.093854Z",
+ "shell.execute_reply": "2025-11-24T18:33:24.093854Z"
+ }
+ },
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
- " 0%| | 0/35 [00:00, ?it/s]"
+ "\r",
+ " 0%| | 0/4 [00:00, ?it/s]"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\r",
+ " 25%|████████████▊ | 1/4 [00:00<00:00, 4.68it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
- "100%|██████████| 35/35 [06:59<00:00, 11.98s/it]\n"
+ "\r",
+ " 50%|█████████████████████████▌ | 2/4 [00:00<00:00, 5.53it/s]"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\r",
+ " 75%|██████████████████████████████████████▎ | 3/4 [00:00<00:00, 6.61it/s]"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\r",
+ "100%|███████████████████████████████████████████████████| 4/4 [00:00<00:00, 7.10it/s]"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\n"
]
}
],
@@ -411,7 +351,7 @@
"\n",
" # For location based\n",
" start_time = time.time()\n",
- " stops_gb = GRID_BASED.grid_based(user_data, time_thresh=240, complete_output=True, timestamp='unix_ts', location_id='h3_cell')\n",
+ " stops_gb = GRID_BASED.grid_based(user_data, time_thresh=240, complete_output=True, traj_cols=tc, location_id='h3_cell')\n",
" execution_time = time.time() - start_time\n",
" results += [pd.Series({'user':user, 'algo':'grid_based', 'execution_time':execution_time, 'n_pings':n_pings})]\n",
" \n",
@@ -440,46 +380,19 @@
"results = pd.DataFrame(results)"
]
},
- {
- "cell_type": "markdown",
- "id": "b329c036-1a08-44ef-8a18-688240087a29",
- "metadata": {},
- "source": [
- "### Use **completeness to normalize** ('hrs with data' / 'total hrs')"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 42,
- "id": "da1a8b6a",
- "metadata": {},
- "outputs": [],
- "source": [
- "completeness_per_user = filters.completeness(traj, timestamp='unix_ts', user_id='gc_identifier')\n",
- "dwell_scaling = 1/completeness_per_user\n",
- "dwell_scaling.name = 'dwell_scaling'\n",
- "\n",
- "metrics = pd.merge(results, dwell_scaling, left_on='user', right_index=True)\n",
- "metrics['rescaled_total_dwell'] = (metrics['total_dwell']/60)*metrics['dwell_scaling'] # in hours"
- ]
- },
{
"cell_type": "code",
- "execution_count": 49,
- "id": "22979688",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAcYAAAHWCAYAAADttCmyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB3DklEQVR4nO3dd3hURdvA4d/29E1v1AQChN6RjoIiIAICIjZQLChVRBCVYgWxIioovgJWmsDrqwIiTUF67zX0kIT0Xnbn+yMfa5YkCGmbhOe+rlyyM7NznkniPplzZs7RKKUUQgghhABA6+gAhBBCiPJEEqMQQgiRhyRGIYQQIg9JjEIIIUQekhiFEEKIPCQxCiGEEHlIYhRCCCHykMQohBBC5CGJUQghhMhDEqOoUDQaDdOmTXN0GKIUdOnShS5dujg6jEINHTqUmjVrOjoMUQYkMQohysyRI0eYNm0aZ8+edXQoBbp8+TLTpk1j3759jg5FOJBG7pUqKpKMjAz0ej16vd7RoYgiWLZsGQMHDmTDhg35ZodZWVkAGI1GB0SWa9euXbRq1Yr58+czdOhQu7rs7GysVismk8kxwYkyI58uokJxcnJydAiilDgyId4Mg8Hg6BBEGZFTqaJIpk2bhkaj4dSpUwwdOhRPT0/MZjNPPPEEaWlpt9TX0KFDcXNz48yZM3Tv3h1XV1eCg4N54403uP6ExvXXGG8ljvT0dEaPHo2vry/u7u7cf//9XLp0KV+fycnJjB07lpo1a2IymfD39+fuu+9mz549hY5h2bJlaDQaNm3alK/uiy++QKPRcOjQIQCuXLnCE088QdWqVTGZTAQFBdGnT5+bOr147NgxBgwYgLe3N05OTrRs2ZKff/7ZVh8dHY2fnx9dunSx+96dOnUKV1dXBg0aZCvLzMxk6tSp1K5dG5PJRLVq1ZgwYQKZmZn5jvvdd9/RunVrXFxc8PLyolOnTvz++++2+sKu/dasWdM281qwYAEDBw4E4M4770Sj0aDRaNi4cSNQ8DXG6Ohohg0bRkBAAE5OTjRp0oSFCxfatTl79iwajYb333+fL7/8klq1amEymWjVqhU7d+781+/pNRs3bqRVq1YAPPHEE7b4FixYAOS/xpj3uJ999hmhoaG4uLhwzz33cOHCBZRSvPnmm1StWhVnZ2f69OlDXFxcvuOuWrWKjh074urqiru7O7169eLw4cM3HbcoeTJjFMXy4IMPEhISwvTp09mzZw9fffUV/v7+vPvuu7fUj8Vi4d577+WOO+5g5syZrF69mqlTp5KTk8Mbb7xRInEMHTqUJUuW8Nhjj3HHHXewadMmevXqla+v4cOHs2zZMkaOHEn9+vWJjY1l8+bNHD16lObNmxd4/F69euHm5saSJUvo3LmzXd3ixYtp0KABDRs2BKB///4cPnyYUaNGUbNmTaKjo1m7di3nz5+/4eKOw4cP0759e6pUqcLLL7+Mq6srS5YsoW/fvvz000/069cPf39/5syZw8CBA5k9ezajR4/GarUydOhQ3N3d+fzzzwGwWq3cf//9bN68mWeeeYbw8HAOHjzIRx99xIkTJ1i5cqXtuK+//jrTpk2jXbt2vPHGGxiNRrZv38769eu55557/vVnc02nTp0YPXo0n3zyCa+88grh4eEAtv9eLz09nS5dunDq1ClGjhxJSEgIS5cuZejQoSQkJDBmzBi79j/88APJyck8++yzaDQaZs6cyQMPPMCZM2duarYXHh7OG2+8wZQpU3jmmWfo2LEjAO3atbvh+77//nuysrIYNWoUcXFxzJw5kwcffJC77rqLjRs3MnHiRE6dOsXs2bMZP348X3/9te293377LUOGDKF79+68++67pKWlMWfOHDp06MDevXtlsY+jKCGKYOrUqQpQTz75pF15v379lI+Pzy31NWTIEAWoUaNG2cqsVqvq1auXMhqNKiYmxlYOqKlTp95yHLt371aAGjt2rF27oUOH5uvTbDarESNG3NIYlFJq8ODByt/fX+Xk5NjKIiMjlVarVW+88YZSSqn4+HgFqPfee++W++/atatq1KiRysjIsJVZrVbVrl07FRYWli8WFxcXdeLECfXee+8pQK1cudJW/+233yqtVqv++usvu/fNnTtXAWrLli1KKaVOnjyptFqt6tevn7JYLHZtrVar7d/Xfw+vqVGjhhoyZIjt9dKlSxWgNmzYkK9t586dVefOnW2vP/74YwWo7777zlaWlZWl2rZtq9zc3FRSUpJSSqmIiAgFKB8fHxUXF2dr+9///lcB6n//+1++YxVm586dClDz58/PVzdkyBBVo0YN2+trx/Xz81MJCQm28kmTJilANWnSRGVnZ9vKBw8erIxGo+3nl5ycrDw9PdXTTz9td5wrV64os9mcr1yUHTmVKopl+PDhdq87duxIbGwsSUlJt9zXyJEjbf/WaDSMHDmSrKws/vjjj2LHsXr1agCef/55u3ajRo3K15enpyfbt2/n8uXLtxT/oEGDiI6Otp0ahNxTrFar1XYK09nZGaPRyMaNG4mPj7/pvuPi4li/fj0PPvggycnJXL16latXrxIbG0v37t05efIkly5dsrX/9NNPMZvNDBgwgMmTJ/PYY4/Rp08fW/3SpUsJDw+nXr16tr6uXr3KXXfdBcCGDRsAWLlyJVarlSlTpqDV2n9caDSaW/r+3KrffvuNwMBABg8ebCszGAyMHj2alJSUfKetBw0ahJeXl+31tRnfmTNnSjXOgQMHYjabba/btGkDwKOPPmq3SKxNmzZkZWXZfk5r164lISGBwYMH2/0MdDodbdq0sf0MRNmTU6miWKpXr273+toHU3x8PB4eHjfdj1arJTQ01K6sTp06ADd17e3f4jh37hxarZaQkBC7drVr187X18yZMxkyZAjVqlWjRYsW9OzZk8cffzxffNe79957MZvNLF68mK5duwK5p1GbNm1qG4vJZOLdd9/lxRdfJCAggDvuuIP77ruPxx9/nMDAwEL7PnXqFEopJk+ezOTJkwtsEx0dTZUqVQDw9vbmk08+YeDAgQQEBPDJJ5/YtT158iRHjx7Fz8+v0L4ATp8+jVarpX79+jcce2k4d+4cYWFh+RLytVOv586dsyu/0e9Aabr+uNeSZLVq1QosvxbPyZMnAWx/jFzvVv7/ESVLEqMoFp1OV2C5KuNdQCUZx4MPPkjHjh1ZsWIFv//+O++99x7vvvsuy5cvp0ePHoW+z2Qy0bdvX1asWMHnn39OVFQUW7Zs4Z133rFrN3bsWHr37s3KlStZs2YNkydPZvr06axfv55mzZoV2LfVagVg/PjxdO/evcA21yf5NWvWALkfxBcvXsTT09Ouv0aNGvHhhx8W2Nf1H+pFZbFYSqSfm+Go38XCjvtv8Vz7mX777bcF/lEkW5IcR77zolywWq2cOXPGNrMCOHHiBECJLECoUaMGVquViIgIwsLCbOWnTp0qsH1QUBDPP/88zz//PNHR0TRv3py33377hokRck/nLVy4kHXr1nH06FGUUnYrQa+pVasWL774Ii+++CInT56kadOmfPDBB3z33XcF9ntttmowGOjWrdu/jnf16tV89dVXTJgwge+//54hQ4awfft224dtrVq12L9/P127dr3hKdFatWphtVo5cuQITZs2LbSdl5cXCQkJdmVZWVlERkbald3K6dcaNWpw4MABrFar3azx2LFjtvqSVtqnh/OqVasWAP7+/jf1MxVlR64xinLj008/tf1bKcWnn36KwWCwnZYsjmuzrGurMq+ZPXu23WuLxUJiYqJdmb+/P8HBwQVuY7het27d8Pb2ZvHixSxevJjWrVvbnb5NS0sjIyPD7j21atXC3d39hv37+/vTpUsXvvjii3zJBiAmJsb274SEBJ566ilat27NO++8w1dffcWePXvsZq4PPvggly5dYt68efn6Sk9PJzU1FYC+ffui1Wp54403bDOca/LOxGrVqsWff/5pV//ll1/mmzG6urraYvw3PXv25MqVKyxevNhWlpOTw+zZs3Fzc8u3+rck3Ep8xdW9e3c8PDx45513yM7Ozlef92cqypbMGEW54OTkxOrVqxkyZAht2rRh1apV/Prrr7zyyiuFXge7FS1atKB///58/PHHxMbG2rZrXJuVXpspJCcnU7VqVQYMGECTJk1wc3Pjjz/+YOfOnXzwwQf/ehyDwcADDzzAokWLSE1N5f3337erP3HiBF27duXBBx+kfv366PV6VqxYQVRUFA899NAN+/7ss8/o0KEDjRo14umnnyY0NJSoqCi2bt3KxYsX2b9/PwBjxowhNjaWP/74A51Ox7333stTTz3FW2+9RZ8+fWjSpAmPPfYYS5YsYfjw4WzYsIH27dtjsVg4duwYS5YsYc2aNbRs2ZLatWvz6quv8uabb9KxY0ceeOABTCYTO3fuJDg4mOnTpwPw1FNPMXz4cPr378/dd9/N/v37WbNmDb6+vnZjaNq0KTqdjnfffZfExERMJhN33XUX/v7++cb7zDPP8MUXXzB06FB2795NzZo1WbZsGVu2bOHjjz/G3d39X38et6pWrVp4enoyd+5c3N3dcXV1pU2bNvmuTZcEDw8P5syZw2OPPUbz5s156KGH8PPz4/z58/z666+0b9/e7o9FUYYctyBWVGTXtknk3UqhlFLz589XgIqIiLjpvoYMGaJcXV3V6dOn1T333KNcXFxUQECAmjp1ar4tAhSyXeNm4khNTVUjRoxQ3t7eys3NTfXt21cdP35cAWrGjBlKKaUyMzPVSy+9pJo0aaLc3d2Vq6ur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YTg5OREQEMCAAQNsdVarlZkzZ1K7dm1MJhPVq1fn7bffttVPnDiROnXq4OLiQmhoKJMnT7Z7TuS0adNo2rQp3377LTVr1sRsNvPQQw+RnJxckt9CIYQolCTGYojLiMuXFK/ZcnkLcRlxpXLcWbNm0bZtW55++mkiIyOJjIykatWqVK1alaVLl3LkyBGmTJnCK6+8wpIlS2zv++CDD1iwYAFff/01mzdvJi4ujhUrVuTrf+HChej1enbs2MGsWbP48MMP+eqrrwDYtWsXo0eP5o033uD48eOsXr2aTp062d47adIkZsyYweTJkzly5Ag//PADAQEBtnp3d3cWLFjAkSNHmDVrFvPmzeOjjz6yO/7p06dZuXIlv/zyC7/88gubNm1ixowZJf1tFEKIgqlKLjExUQEqMTExX116ero6cuSISk9PL1Lf+6P3q4YLGhb6tT96f3HDL1Tnzp3VmDFjbthmxIgRqn///rbXQUFBaubMmbbX2dnZqmrVqqpPnz52/YaHhyur1WormzhxogoPD1dKKfXTTz8pDw8PlZSUlO94SUlJymQyqXnz5t30ON577z3VokUL2+upU6cqFxcXu/5feukl1aZNm5vu898U9+cuhKiYbpQP8pIZYzG4G92LVV/SPvvsM1q0aIGfnx9ubm58+eWXnD9/HoDExEQiIyNp06aNrb1er6dly5b5+rnjjjvQaDS2123btuXkyZNYLBbuvvtuatSoQWhoKI899hjff/89aWlpABw9epTMzEy6du1aaIyLFy+mffv2BAYG4ubmxmuvvWaL8ZqaNWvi7v7P9y4oKIjo6OiifVOEEOIWSWIsBm8nb9oHty+wrn1we7ydvMsslkWLFjF+/HiGDRvG77//zr59+3jiiSfIysoq0eO4u7uzZ88efvzxR4KCgpgyZQpNmjQhISEBZ2fnG75369atPPLII/Ts2ZNffvmFvXv38uqrr+aL0WAw2L3WaDRYrdYSHYcQQhRGEmMxmE1mprWbli85XluVajaZS+3YRqMRi8Vie71lyxbatWvH888/T7NmzahduzanT5/+J1azmaCgILZv324ry8nJYffu3fn6ztsGYNu2bYSFhaHT6YDcmWa3bt2YOXMmBw4c4OzZs6xfv56wsDCcnZ1Zt25dgTH//fff1KhRg1dffZWWLVsSFhbGuXPnivV9EEKIkibbNYop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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "execution_count": 7,
+ "id": "8e8546e8",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:24.093854Z",
+ "iopub.status.busy": "2025-11-24T18:33:24.093854Z",
+ "iopub.status.idle": "2025-11-24T18:33:24.338240Z",
+ "shell.execute_reply": "2025-11-24T18:33:24.338240Z"
}
- ],
+ },
+ "outputs": [],
"source": [
"import seaborn as sns\n",
"\n",
@@ -487,13 +400,16 @@
"palette = dict(zip(algos, sns.color_palette(n_colors=len(algos))))\n",
"\n",
"fig, ax = plt.subplots(figsize=(5, 5))\n",
- "sns.scatterplot(data=metrics, x='n_pings', y='execution_time', hue='algo', ax=ax)\n",
+ "sns.scatterplot(data=results, x='n_pings', y='execution_time', hue='algo', ax=ax)\n",
"ax.set_title('n_pings vs execution_time')\n",
"plt.show()"
]
}
],
"metadata": {
+ "jupytext": {
+ "formats": "ipynb,py:percent"
+ },
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
@@ -509,7 +425,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.10.0"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/examples/benchmarking_of_stop_detection_algorithms.py b/examples/benchmarking_of_stop_detection_algorithms.py
new file mode 100644
index 00000000..5972b6a1
--- /dev/null
+++ b/examples/benchmarking_of_stop_detection_algorithms.py
@@ -0,0 +1,198 @@
+# ---
+# jupyter:
+# jupytext:
+# formats: ipynb,py:percent
+# text_representation:
+# extension: .py
+# format_name: percent
+# format_version: '1.3'
+# jupytext_version: 1.17.3
+# kernelspec:
+# display_name: Python 3 (ipykernel)
+# language: python
+# name: python3
+# ---
+
+# %% [markdown]
+# # Comparing runtimes of different stop detection algorithms on toy datasets
+
+# %% [markdown]
+# Here we compare the runtimes of four different stop detection algorithms: Lachesis, grid-based, temporal DBSCAN, and HDBSCAN.
+
+# %%
+# %matplotlib inline
+import matplotlib
+matplotlib.use('TkAgg')
+import matplotlib.pyplot as plt
+plt.ion()
+
+# Imports
+import nomad.io.base as loader
+import geopandas as gpd
+from shapely.geometry import box
+import pandas as pd
+import numpy as np
+from nomad.stop_detection.viz import plot_stops_barcode, plot_pings, plot_stops, plot_time_barcode
+import nomad.stop_detection.dbscan as DBSCAN
+import nomad.stop_detection.lachesis as LACHESIS
+import nomad.stop_detection.grid_based as GRID_BASED
+import nomad.stop_detection.hdbscan as HDBSCAN
+import nomad.filters as filters
+import nomad.stop_detection.postprocessing as post
+import time
+from tqdm import tqdm
+
+# Load data
+import nomad.data as data_folder
+from pathlib import Path
+data_dir = Path(data_folder.__file__).parent
+city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')
+outer_box = box(*city.total_bounds).buffer(15, join_style='mitre')
+
+filepath_root = 'gc_data_long/'
+tc = {
+ "user_id": "gc_identifier",
+ "timestamp": "unix_ts",
+ "x": "dev_x",
+ "y": "dev_y",
+ "ha":"ha",
+ "date":"date"}
+
+users = ['admiring_brattain']
+traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)
+
+# Lachesis (sequential stop detection)
+start_time = time.time()
+stops = LACHESIS.lachesis(traj, delta_roam=20, dt_max = 60, dur_min=5, complete_output=True, keep_col_names=True, traj_cols=tc)
+execution_time_lachesis = time.time() - start_time
+print(f"Lachesis execution time: {execution_time_lachesis} seconds")
+
+# Density based stop detection (Temporal DBSCAN)
+start_time = time.time()
+user_data_tadb = traj.assign(cluster=DBSCAN.ta_dbscan_labels(traj, time_thresh=240, dist_thresh=15, min_pts=3, traj_cols=tc))
+clustering_time_tadbscan = time.time() - start_time
+start_time_post = time.time()
+cluster_labels_tadb = post.remove_overlaps(user_data_tadb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)
+execution_time_tadbscan = time.time() - start_time
+post_time_tadbscan = time.time() - start_time_post
+print(f"TA-DBSCAN execution time: {execution_time_tadbscan} seconds")
+print(f"TA-DBSCAN clustering time: {clustering_time_tadbscan} seconds")
+print(f"TA-DBSCAN post-processing time: {post_time_tadbscan} seconds")
+
+# Grid-based
+start_time = time.time()
+traj['h3_cell'] = filters.to_tessellation(traj, index="h3", res=10, traj_cols=tc, data_crs='EPSG:3857')
+stops_gb = GRID_BASED.grid_based(traj, time_thresh=240, complete_output=True, traj_cols=tc, location_id='h3_cell')
+execution_time_grid = time.time() - start_time
+print(f"Grid-Based execution time: {execution_time_grid} seconds")
+
+# HDBSCAN
+start_time = time.time()
+user_data_hdb = traj.assign(cluster=HDBSCAN.hdbscan_labels(traj, time_thresh=240, min_pts=3, min_cluster_size=2, traj_cols=tc))
+clustering_time_hdbscan = time.time() - start_time
+start_time_post = time.time()
+cluster_labels_hdb = post.remove_overlaps(user_data_hdb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)
+execution_time_hdbscan = time.time() - start_time
+post_time_hdbscan = time.time() - start_time_post
+print(f"HDBSCAN execution time: {execution_time_hdbscan} seconds")
+print(f"HDBSCAN clustering time: {clustering_time_hdbscan} seconds")
+print(f"HDBSCAN post-processing time: {post_time_hdbscan} seconds")
+
+# %% [markdown]
+# ## Summary of Single-User Performance
+
+# %% [markdown]
+# ### Lachesis
+
+# %%
+fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),
+ gridspec_kw={'height_ratios':[10,1]})
+
+gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')
+city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')
+
+plot_stops(stops, ax=ax_map, cmap='Reds')
+plot_pings(traj, ax=ax_map, s=6, point_color='black', cmap='twilight', traj_cols=tc)
+ax_map.set_axis_off()
+
+plot_time_barcode(traj[tc['timestamp']], ax=ax_barcode, set_xlim=True)
+plot_stops_barcode(stops, ax=ax_barcode, cmap='Reds', set_xlim=False, timestamp='unix_ts')
+
+plt.tight_layout(pad=0.1)
+plt.show()
+
+# %%
+print("Summary of Single-User Performance")
+print(f"Lachesis execution time: {execution_time_lachesis} seconds")
+print(f"TA-DBSCAN execution time: {execution_time_tadbscan} seconds")
+print(f"Grid-Based execution time: {execution_time_grid} seconds")
+print(f"HDBSCAN execution time: {execution_time_hdbscan} seconds")
+
+# %%
+print("Runtime Disaggregation")
+print(f"Lachesis clustering time: {execution_time_lachesis} seconds")
+print("--------------------------------")
+print(f"TA-DBSCAN clustering time: {clustering_time_tadbscan} seconds")
+print(f"TA-DBSCAN post-processing time: {post_time_tadbscan} seconds")
+print("--------------------------------")
+print(f"Grid-Based clustering time: {execution_time_grid} seconds")
+print("--------------------------------")
+print(f"HDBSCAN clustering time: {clustering_time_hdbscan} seconds")
+print(f"HDBSCAN post-processing time: {post_time_hdbscan} seconds")
+
+# %% [markdown]
+# ## Pings vs Runtime
+
+# %%
+traj = loader.sample_from_file(filepath_root, frac_users=0.1, format='parquet', traj_cols=tc, seed=10)
+
+# H3 cells for grid_based stop detection method
+traj['h3_cell'] = filters.to_tessellation(traj, index="h3", res=10, traj_cols=tc, data_crs='EPSG:3857')
+pings_per_user = traj['gc_identifier'].value_counts()
+
+# %%
+# Approximately 5 minutes for 40 users
+results = []
+for user, n_pings in tqdm(pings_per_user.items(), total=len(pings_per_user)):
+ user_data = traj.query("gc_identifier == @user")
+
+ # For location based
+ start_time = time.time()
+ stops_gb = GRID_BASED.grid_based(user_data, time_thresh=240, complete_output=True, traj_cols=tc, location_id='h3_cell')
+ execution_time = time.time() - start_time
+ results += [pd.Series({'user':user, 'algo':'grid_based', 'execution_time':execution_time, 'n_pings':n_pings})]
+
+ # For Lachesis
+ start_time = time.time()
+ stops_lac = LACHESIS.lachesis(user_data, delta_roam=30, dt_max=240, complete_output=True, traj_cols=tc)
+ execution_time = time.time() - start_time
+ results += [pd.Series({'user':user, 'algo':'lachesis', 'execution_time':execution_time, 'n_pings':n_pings})]
+
+ # For TADbscan
+ start_time = time.time()
+ user_data_tadb = user_data.assign(cluster=DBSCAN.ta_dbscan_labels(user_data, time_thresh=240, dist_thresh=15, min_pts=3, traj_cols=tc))
+ # - post-processing
+ stops_tadb = post.remove_overlaps(user_data_tadb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)
+ execution_time = time.time() - start_time
+ results += [pd.Series({'user':user, 'algo':'tadbscan', 'execution_time':execution_time, 'n_pings':n_pings})]
+
+ # For HDBSCAN
+ start_time = time.time()
+ user_data_hdb = user_data.assign(cluster=HDBSCAN.hdbscan_labels(user_data, time_thresh=240, min_pts=3, min_cluster_size=2, traj_cols=tc))
+ # - post-processing
+ stops_hdb = post.remove_overlaps(user_data_hdb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)
+ execution_time = time.time() - start_time
+ results += [pd.Series({'user':user, 'algo':'hdbscan', 'execution_time':execution_time, 'n_pings':n_pings})]
+
+results = pd.DataFrame(results)
+
+# %%
+import seaborn as sns
+
+algos = ['grid_based', 'lachesis', 'tadbscan', 'hdbscan']
+palette = dict(zip(algos, sns.color_palette(n_colors=len(algos))))
+
+fig, ax = plt.subplots(figsize=(5, 5))
+sns.scatterplot(data=results, x='n_pings', y='execution_time', hue='algo', ax=ax)
+ax.set_title('n_pings vs execution_time')
+plt.show()
diff --git a/examples/generate_synthetic_pois.ipynb b/examples/generate_synthetic_pois.ipynb
index c9e8d88c..1fec4f56 100644
--- a/examples/generate_synthetic_pois.ipynb
+++ b/examples/generate_synthetic_pois.ipynb
@@ -2,8 +2,8 @@
"cells": [
{
"cell_type": "code",
- "execution_count": 2,
- "id": "0eb89ec7-0bed-48fe-9005-d54c14695ff7",
+ "execution_count": null,
+ "id": "e7c495ca",
"metadata": {},
"outputs": [],
"source": [
@@ -15,36 +15,10 @@
},
{
"cell_type": "code",
- "execution_count": 3,
- "id": "71e396a0-5d62-4183-9ae0-5b15c8ca9853",
+ "execution_count": null,
+ "id": "e96465b0",
"metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "C:\\Users\\pacob\\Desktop\\Brain\\Code Development\\nomad\\nomad\\city_gen.py:271: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.\n",
- " self.buildings_gdf = pd.concat([self.buildings_gdf, new_row], axis=0)\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "City built and street graph computed in 0.952s; buildings=106 streets=224\n"
- ]
- },
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
+ "outputs": [],
"source": [
"# Initialize city and time the build\n",
"t0 = time.perf_counter()\n",
@@ -73,37 +47,37 @@
"city.add_building('home', (8, 10), blocks=[(7, 10), (7, 9)])\n",
"\n",
"# add workplaces\n",
- "city.add_building('work', (3, 4), blocks=[(4, 4), (4, 5)])\n",
- "city.add_building('work', (5, 3), blocks=[(5, 4), (5, 5)])\n",
- "city.add_building('work', (6, 6), geom=box(6, 4, 8, 6))\n",
- "city.add_building('work', (8, 6), geom=box(8, 4, 10, 6))\n",
- "city.add_building('work', (12, 6), geom=box(11, 5, 14, 6))\n",
- "city.add_building('work', (12, 3), geom=box(11, 4, 14, 5))\n",
- "city.add_building('work', (15, 3), geom=box(14, 4, 17, 6))\n",
- "city.add_building('work', (18, 4), geom=box(17, 4, 18, 6))\n",
- "city.add_building('work', (18, 6), geom=box(16, 6, 18, 8))\n",
- "city.add_building('work', (15, 9), geom=box(16, 8, 17, 10))\n",
- "city.add_building('work', (18, 8), geom=box(17, 8, 18, 10))\n",
- "city.add_building('work', (18, 10), geom=box(16, 10, 18, 12))\n",
- "city.add_building('work', (18, 13), geom=box(16, 13, 18, 15))\n",
- "city.add_building('work', (18, 15), geom=box(16, 15, 18, 16))\n",
- "city.add_building('work', (15, 15), geom=box(15, 16, 18, 17))\n",
- "city.add_building('work', (14, 15), blocks=[(14, 16)])\n",
- "city.add_building('work', (16, 18), geom=box(16, 17, 18, 18))\n",
- "city.add_building('work', (15, 18), geom=box(14, 17, 16, 18))\n",
- "city.add_building('work', (13, 18), geom=box(12, 16, 14, 18))\n",
- "city.add_building('work', (11, 18), geom=box(10, 17, 12, 18))\n",
- "city.add_building('work', (11, 15), geom=box(10, 16, 12, 17))\n",
- "city.add_building('work', (8, 18), geom=box(7, 16, 9, 18))\n",
- "city.add_building('work', (6, 18), geom=box(5, 17, 7, 18))\n",
- "city.add_building('work', (6, 15), geom=box(5, 16, 7, 17))\n",
- "city.add_building('work', (3, 16), blocks=[(4, 16), (4, 17)])\n",
- "city.add_building('work', (3, 13), geom=box(4, 13, 6, 16))\n",
- "city.add_building('work', (6, 12), geom=box(4, 12, 6, 13))\n",
- "city.add_building('work', (3, 10), blocks=[(4, 9), (4, 10)])\n",
- "city.add_building('work', (6, 9), blocks=[(5, 9), (5, 10)])\n",
- "city.add_building('work', (6, 8), blocks=[(4, 8), (5, 8)])\n",
- "city.add_building('work', (3, 6), geom=box(4, 6, 6, 8))\n",
+ "city.add_building('workplace', (3, 4), blocks=[(4, 4), (4, 5)])\n",
+ "city.add_building('workplace', (5, 3), blocks=[(5, 4), (5, 5)])\n",
+ "city.add_building('workplace', (6, 6), geom=box(6, 4, 8, 6))\n",
+ "city.add_building('workplace', (8, 6), geom=box(8, 4, 10, 6))\n",
+ "city.add_building('workplace', (12, 6), geom=box(11, 5, 14, 6))\n",
+ "city.add_building('workplace', (12, 3), geom=box(11, 4, 14, 5))\n",
+ "city.add_building('workplace', (15, 3), geom=box(14, 4, 17, 6))\n",
+ "city.add_building('workplace', (18, 4), geom=box(17, 4, 18, 6))\n",
+ "city.add_building('workplace', (18, 6), geom=box(16, 6, 18, 8))\n",
+ "city.add_building('workplace', (15, 9), geom=box(16, 8, 17, 10))\n",
+ "city.add_building('workplace', (18, 8), geom=box(17, 8, 18, 10))\n",
+ "city.add_building('workplace', (18, 10), geom=box(16, 10, 18, 12))\n",
+ "city.add_building('workplace', (18, 13), geom=box(16, 13, 18, 15))\n",
+ "city.add_building('workplace', (18, 15), geom=box(16, 15, 18, 16))\n",
+ "city.add_building('workplace', (15, 15), geom=box(15, 16, 18, 17))\n",
+ "city.add_building('workplace', (14, 15), blocks=[(14, 16)])\n",
+ "city.add_building('workplace', (16, 18), geom=box(16, 17, 18, 18))\n",
+ "city.add_building('workplace', (15, 18), geom=box(14, 17, 16, 18))\n",
+ "city.add_building('workplace', (13, 18), geom=box(12, 16, 14, 18))\n",
+ "city.add_building('workplace', (11, 18), geom=box(10, 17, 12, 18))\n",
+ "city.add_building('workplace', (11, 15), geom=box(10, 16, 12, 17))\n",
+ "city.add_building('workplace', (8, 18), geom=box(7, 16, 9, 18))\n",
+ "city.add_building('workplace', (6, 18), geom=box(5, 17, 7, 18))\n",
+ "city.add_building('workplace', (6, 15), geom=box(5, 16, 7, 17))\n",
+ "city.add_building('workplace', (3, 16), blocks=[(4, 16), (4, 17)])\n",
+ "city.add_building('workplace', (3, 13), geom=box(4, 13, 6, 16))\n",
+ "city.add_building('workplace', (6, 12), geom=box(4, 12, 6, 13))\n",
+ "city.add_building('workplace', (3, 10), blocks=[(4, 9), (4, 10)])\n",
+ "city.add_building('workplace', (6, 9), blocks=[(5, 9), (5, 10)])\n",
+ "city.add_building('workplace', (6, 8), blocks=[(4, 8), (5, 8)])\n",
+ "city.add_building('workplace', (3, 6), geom=box(4, 6, 6, 8))\n",
"\n",
"# add retail places\n",
"city.add_building('retail', (0, 1), geom=box(1, 1, 3, 3))\n",
@@ -167,7 +141,7 @@
"print(f\"City built and street graph computed in {elapsed:.3f}s; buildings={len(city.buildings_gdf)} streets={len(city.streets_gdf)}\")\n",
"\n",
"# Persist as GeoPackage\n",
- "city.save_geopackage('synthetic_pois.gpkg')\n",
+ "city.save_geopackage('garden-city.gpkg')\n",
"\n",
"# Plot a city\n",
"fig, ax = plt.subplots(figsize=(6, 6))\n",
@@ -179,7 +153,7 @@
},
{
"cell_type": "markdown",
- "id": "4b65abc5-4e5f-4376-88ab-998caca51d57",
+ "id": "12f6be66",
"metadata": {},
"source": [
"## Plotting a shortest path"
@@ -187,8 +161,8 @@
},
{
"cell_type": "code",
- "execution_count": 30,
- "id": "c599d0f0-5b20-4627-a015-daa7cc3e35d3",
+ "execution_count": null,
+ "id": "c8f01501",
"metadata": {},
"outputs": [],
"source": [
@@ -207,18 +181,6 @@
"display_name": "Python (nomad repo venv)",
"language": "python",
"name": "nomad-repo-venv"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/examples/generate_synthetic_trajectories.ipynb b/examples/generate_synthetic_trajectories.ipynb
index 1ee4a1cc..2fd681c3 100644
--- a/examples/generate_synthetic_trajectories.ipynb
+++ b/examples/generate_synthetic_trajectories.ipynb
@@ -1,856 +1,247 @@
{
"cells": [
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "2ccfc0c4",
- "metadata": {
- "tags": []
- },
- "outputs": [],
- "source": [
- "import pandas as pd\n",
- "import numpy as np\n",
- "from datetime import datetime, timedelta\n",
- "from zoneinfo import ZoneInfo\n",
- "import matplotlib.pyplot as plt\n",
- "plt.style.use('seaborn-v0_8-muted')\n",
- "from matplotlib import cm\n",
- "import geopandas as gpd\n",
- "\n",
- "from pyproj import Transformer\n",
- "from concurrent.futures import ProcessPoolExecutor\n",
- "import concurrent.futures\n",
- "import multiprocessing\n",
- "from multiprocessing import Pool\n",
- "from functools import partial\n",
- "import numpy.random as npr\n",
- "import matplotlib.dates as mdates\n",
- "from itertools import product\n",
- "import copy\n",
- "import pickle\n",
- "from tqdm import tqdm\n",
- "\n",
- "import nomad.io.base as loader\n",
- "import nomad.city_gen as cg\n",
- "from nomad.city_gen import City, Building\n",
- "import nomad.traj_gen as tg\n",
- "from nomad.traj_gen import Agent, Population\n",
- "import nomad.stop_detection.ta_dbscan as DBSCAN\n",
- "import nomad.stop_detection.lachesis as Lachesis\n",
- "from nomad.generation.sparsity import gen_params_target_q"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "3fd6c945",
- "metadata": {
- "tags": []
- },
- "outputs": [],
- "source": [
- "# garden city\n",
- "\n",
- "city_geojson = gpd.read_file('garden_city.geojson')\n",
- "\n",
- "city = cg.load('garden-city.pkl')"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "id": "3e5ee980",
- "metadata": {
- "tags": []
- },
- "outputs": [],
- "source": [
- "# synthetic philly\n",
- "\n",
- "city_geojson = gpd.read_file('philly.geojson')\n",
- "\n",
- "s3 = boto3.client('s3', region_name=\"us-east-2\")\n",
- "pickle_buffer = io.BytesIO()\n",
- "s3.download_fileobj(\"synthetic-philly\", \"philadelphia-city.pkl\", pickle_buffer)\n",
- "pickle_buffer.seek(0)\n",
- "city = pickle.load(pickle_buffer)"
- ]
- },
{
"cell_type": "markdown",
- "id": "eb7675c1",
+ "id": "10e2517a",
"metadata": {},
"source": [
- "### Generate N agents"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "b76a575b",
- "metadata": {},
- "source": [
- "The following code maps our Garden City coordinates to a location in the Atlantic Ocean (Atlantis?)."
+ "# Synthetic Trajectory Generation with Nomad\n",
+ "\n",
+ "This notebook demonstrates how to generate realistic synthetic human mobility trajectories."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "e21366a2",
- "metadata": {
- "tags": []
- },
+ "id": "58d68e64",
+ "metadata": {},
"outputs": [],
"source": [
- "def garden_city_to_lat_long(agent, sparse_traj=True, full_traj=False, diaries=True):\n",
- " def project_city_blocks_to_web_mercator(df):\n",
- " \"\"\"Convert (x, y) from 15m block units to Web Mercator meters via affine shift and projection.\"\"\"\n",
- " transformer = Transformer.from_crs(\"EPSG:3857\", \"EPSG:4326\", always_xy=True)\n",
- " df['x'] = 15 * df['x'] - 4265699\n",
- " df['y'] = 15 * df['y'] + 4392976\n",
- " if 'ha' in df:\n",
- " df['ha'] = 15 * df['ha']\n",
- " df['longitude'], df['latitude'] = transformer.transform(df['x'].values, df['y'].values)\n",
- " df['date'] = df['datetime'].dt.date\n",
- " return df\n",
- "\n",
- " def finalize(df):\n",
- " front = ['user_id', 'timestamp', 'longitude', 'latitude', 'x', 'y', 'date']\n",
- " cols = [col for col in front if col in df] + [col for col in df.columns if col not in front]\n",
- " return df[cols].rename(columns={'user_id': 'user_id', 'timestamp': 'timestamp'}).reset_index(drop=True)\n",
+ "import pandas as pd\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.style.use('default')\n",
+ "import time\n",
+ "import os\n",
+ "from joblib import Parallel, delayed\n",
"\n",
- " if sparse_traj:\n",
- " agent.sparse_traj = finalize(project_city_blocks_to_web_mercator(agent.sparse_traj))\n",
- " if full_traj:\n",
- " agent.trajectory = finalize(project_city_blocks_to_web_mercator(agent.trajectory))\n",
- " \n",
- " if diaries:\n",
- " diary = agent.diary.copy()\n",
- " xs = []\n",
- " ys = []\n",
- " for loc in diary[\"location\"]:\n",
- " if loc is None:\n",
- " xs.append(None)\n",
- " ys.append(None)\n",
- " else:\n",
- " pt = agent.city.buildings[loc].geometry.centroid\n",
- " xs.append(pt.x)\n",
- " ys.append(pt.y)\n",
- " diary[\"x\"] = xs\n",
- " diary[\"y\"] = ys\n",
- " agent.diary = finalize(project_city_blocks_to_web_mercator(diary))"
+ "from nomad.city_gen import City\n",
+ "from nomad.traj_gen import Agent, Population\n",
+ "from nomad.stop_detection.viz import plot_pings, plot_time_barcode"
]
},
{
"cell_type": "code",
- "execution_count": 22,
- "id": "3d0fe233",
- "metadata": {
- "tags": []
- },
+ "execution_count": null,
+ "id": "9860e901",
+ "metadata": {},
"outputs": [],
"source": [
- "def philly_to_lat_long(agent, sparse_traj=True, full_traj=False, diaries=True):\n",
- " def project_point_to_web_mercator(x, y):\n",
- " \"\"\"\n",
- " Project a fractional (x, y) block coord to Web Mercator using affine interpolation.\n",
- " philly_grid_map is the grid_map produced by RealCityGenerator in virtual_philly.ipynb\n",
- " import it into this notebook through pkl\n",
- " \"\"\"\n",
- " i, j = int(math.floor(x)), int(math.floor(y))\n",
- " poly = philly_grid_map.get((i, j))\n",
- "\n",
- " if poly is None:\n",
- " raise ValueError(f\"No polygon found at grid cell ({i}, {j})\")\n",
- "\n",
- " # Bounds of the 1x1 block polygon in EPSG:3857\n",
- " minx, miny, maxx, maxy = poly.bounds\n",
+ "city = City.from_geopackage('garden-city.gpkg', edges_path='garden-city-edges.parquet')\n",
+ "city._build_hub_network(hub_size=16)\n",
+ "city.compute_gravity(exponent=2.0)\n",
"\n",
- " dx = x - i\n",
- " dy = y - j\n",
- "\n",
- " X = minx + dx * (maxx - minx)\n",
- " Y = miny + dy * (maxy - miny)\n",
- "\n",
- " return X, Y\n",
- "\n",
- " def apply_projection_to_df(df):\n",
- " \"\"\"Apply Web Mercator projection to a DataFrame with 'x' and 'y' columns.\"\"\"\n",
- " def safe_project(row):\n",
- " try:\n",
- " return project_point_to_web_mercator(row['x'], row['y'])\n",
- " except Exception:\n",
- " return (None, None)\n",
- "\n",
- " projected = df.apply(safe_project, axis=1)\n",
- " df[['x', 'y']] = pd.DataFrame(projected.tolist(), index=df.index)\n",
- "\n",
- " transformer = Transformer.from_crs(\"EPSG:3857\", \"EPSG:4326\", always_xy=True)\n",
- " if 'ha' in df:\n",
- " df['ha'] = 10 * df['ha'] # 10 because thats the sidelength of a block\n",
- " df['longitude'], df['latitude'] = transformer.transform(df['x'].values, df['y'].values)\n",
- " df['date'] = df['datetime'].dt.date\n",
- " return df\n",
- "\n",
- " def finalize(df):\n",
- " front = ['identifier', 'timestamp', 'longitude', 'latitude', 'x', 'y', 'date']\n",
- " cols = [col for col in front if col in df] + [col for col in df.columns if col not in front]\n",
- " return df[cols].rename(columns={'identifier': 'uid', 'timestamp': 'timestamp'}).reset_index(drop=True)\n",
- "\n",
- " if sparse_traj:\n",
- " agent.sparse_traj = finalize(apply_projection_to_df(agent.sparse_traj))\n",
- " if full_traj:\n",
- " agent.trajectory = finalize(apply_projection_to_df(agent.trajectory))\n",
- "\n",
- " if diaries:\n",
- " diary = agent.diary.copy()\n",
- " xs = []\n",
- " ys = []\n",
- " for loc in diary[\"location\"]:\n",
- " if loc is None:\n",
- " xs.append(None)\n",
- " ys.append(None)\n",
- " else:\n",
- " pt = agent.city.buildings[loc].geometry.centroid\n",
- " xs.append(pt.x)\n",
- " ys.append(pt.y)\n",
- " diary[\"x\"] = xs\n",
- " diary[\"y\"] = ys\n",
- " agent.diary = finalize(apply_projection_to_df(diary))"
+ "print(f\"City: {city.name}\")\n",
+ "print(f\"Dimensions: {city.dimensions}\")\n",
+ "print(f\"Buildings: {len(city.buildings_gdf)}\")"
]
},
{
"cell_type": "markdown",
- "id": "eb8c0db6",
+ "id": "9ff8ddec",
"metadata": {},
"source": [
- "## Simple trajectory generation\n",
+ "## Part 1: Effect of Sampling Parameters on Sparsity\n",
"\n",
- "For simple trajectory generation tasks that don't require too much computation power and can be done on a personal laptop, the following code generates a trajectory for each agent and saves it to a csv."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 26,
- "id": "82f2112f",
- "metadata": {
- "tags": []
- },
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "Generating trajectories: 100%|██████████| 1/1 [00:28<00:00, 28.22s/it]\n"
- ]
- }
- ],
- "source": [
- "def generate_trajectory_data(agent, seed_trajectory=None, seed_sparsity=None):\n",
- " beta_params = gen_params_target_q(q_range=(0.3, 0.9), seed=seed_sparsity)\n",
- " rng = npr.default_rng(seed_sparsity)\n",
- " ha_sample = rng.uniform(11.5/15, 1)\n",
- "\n",
- " agent.reset_trajectory()\n",
- " agent.generate_trajectory(\n",
- " datetime = \"2024-01-01T07:00 -04:00\",\n",
- " end_time = pd.Timestamp('2024-01-31T09:00 -04:00'),\n",
- " seed=seed_trajectory,\n",
- " dt=1)\n",
- "\n",
- " agent.sample_trajectory(\n",
- " **beta_params,\n",
- " seed=seed_sparsity,\n",
- " ha=ha_sample,\n",
- " replace_sparse_traj=True)\n",
- "\n",
- " philly_to_lat_long(agent, sparse_traj=True, full_traj=False)\n",
- " agent.reset_trajectory(trajectory = False, sparse = False, diary = False)\n",
- " return None\n",
- "\n",
- "# Generate trajectories with progress bar\n",
- "N = 1\n",
- "population = Population(city)\n",
- "population.generate_agents(N=N, seed=250, name_count=2)\n",
- "\n",
- "for i, agent in enumerate(tqdm(population.roster.values(), desc=\"Generating trajectories\")):\n",
- " generate_trajectory_data(agent, seed_trajectory=i, seed_sparsity=i)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 27,
- "id": "cda6fba3",
- "metadata": {
- "tags": []
- },
- "outputs": [],
- "source": [
- "agent.sparse_traj.to_csv(\"philly_sparse_traj.csv\", index=False)\n",
- "agent.trajectory.to_csv(\"philly_full_traj.csv\", index=False)"
+ "Generate 3 agents with 2-day trajectories, varying beta_duration and beta_start \n",
+ "to show their effect on sparsity (q = observed points / ground truth points)."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "8f47ea1c",
+ "id": "b6336cc1",
"metadata": {},
"outputs": [],
"source": [
- "def generate_agent_trajectory(agent_id, agent, seed):\n",
+ "np.random.seed(42)\n",
+ "population = Population(city)\n",
+ "population.generate_agents(N=3, seed=42, name_count=2)\n",
"\n",
- " beta_params = gen_params_target_q(q_range=(0.4, 0.85), seed=seed)\n",
+ "# Vary beta_duration and beta_start to target different sparsity levels\n",
+ "sampling_params = [\n",
+ " {'beta_ping': 5, 'beta_start': 100, 'beta_durations': 60}, \n",
+ " {'beta_ping': 5, 'beta_start': 250, 'beta_durations': 150}, \n",
+ " {'beta_ping': 5, 'beta_start': 400, 'beta_durations': 240} \n",
+ "]\n",
"\n",
+ "# Generate 2-day trajectories for quick visualization\n",
+ "for i, (agent_id, agent) in enumerate(population.roster.items()):\n",
" agent.generate_trajectory(\n",
- " datetime = \"2024-01-01T08:00 -04:00\",\n",
- " end_time = pd.Timestamp('2024-01-21T08:30:00 -04:00'),\n",
- " seed=1,\n",
- " dt=0.25)\n",
- " print('finished generating trajectory')\n",
+ " datetime=pd.Timestamp(\"2024-01-01T07:00-04:00\"),\n",
+ " end_time=pd.Timestamp(\"2024-01-03T07:00-04:00\"),\n",
+ " seed=i\n",
+ " )\n",
+ "\n",
" agent.sample_trajectory(\n",
- " **beta_params,\n",
- " seed=seed,\n",
- " ha=13/15, # <<<<<<\n",
- " replace_sparse_traj=True)\n",
+ " **sampling_params[i],\n",
+ " replace_sparse_traj=True,\n",
+ " seed=i\n",
+ " )\n",
" \n",
- " garden_city_to_lat_long(agent,\n",
- " sparse_traj=True,\n",
- " full_traj=False)\n",
- " agent.reset_trajectory(trajectory = True, sparse = False, diary = False)\n",
- " \n",
- " return agent_id, copy.deepcopy(agent)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "d0f8a946",
- "metadata": {},
- "outputs": [],
- "source": [
- "population = Population(city)\n",
- "population.generate_agents(N=2, seed=2, name_count=2)"
+ " q = len(agent.sparse_traj) / len(agent.trajectory)\n",
+ " print(f\"Agent {i}: q={q:.3f}, beta_start={sampling_params[i]['beta_start']}, \"\n",
+ " f\"beta_dur={sampling_params[i]['beta_durations']}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "4946528f",
+ "id": "0e4b20fa",
"metadata": {
- "scrolled": true
+ "lines_to_next_cell": 1
},
"outputs": [],
"source": [
- "%%time \n",
- "# time for 2 weeks of data at dt = 0.25\n",
- "agent_1 = list(population.roster.values())[1]\n",
- "generate_agent_trajectory(agent_1, 10, 6)\n",
- "agent_1.sparse_traj.head()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "fbb51dfd",
- "metadata": {},
- "outputs": [],
- "source": [
- "def generate_trajectory_data(agent, seed_trajectory=None, seed_sparsity=None, use_datetime=None, use_lon_lat=True):\n",
- " beta_params = gen_params_ranges(seed=seed_sparsity)\n",
- " rng = npr.default_rng(seed_sparsity)\n",
- " ha_sample = rng.uniform(11.5/15, 16.5/15)\n",
- "\n",
- " agent.reset_trajectory()\n",
- " agent.generate_trajectory(\n",
- " datetime = \"2024-01-01T07:00 -04:00\",\n",
- " end_time = pd.Timestamp('2024-01-15T09:00:00 -04:00'),\n",
- " seed=seed_trajectory,\n",
- " dt=1)\n",
- "\n",
- " agent.sample_trajectory(\n",
- " **beta_params,\n",
- " seed=seed_sparsity,\n",
- " ha=ha_sample,\n",
- " replace_sparse_traj=True)\n",
- "\n",
- " garden_city_to_lat_long(agent, sparse_traj=True, full_traj=False, use_datetime=use_datetime, use_lon_lat=use_lon_lat)\n",
- " agent.reset_trajectory(trajectory = True, sparse = False, diary = False)\n",
- " return None\n",
- "\n",
- "# Generate trajectories with progress bar\n",
- "N = 100 \n",
- "population = Population(city)\n",
- "population.generate_agents(N=N, seed=314, name_count=2)\n",
+ "fig, axes = plt.subplots(2, 3, figsize=(15, 10), \n",
+ " gridspec_kw={'height_ratios': [10, 1]})\n",
"\n",
- "for i, agent in enumerate(tqdm(population.roster.values(), desc=\"Generating trajectories\")):\n",
- " generate_trajectory_data(agent, seed_trajectory=i, seed_sparsity=i, use_datetime=None, use_lon_lat=True)\n",
- " #agent.sparse_traj.rename(columns={'uid': 'identifier', 'timestamp': 'unix_timestamp', 'latitude':'device_lat', 'longitude':'device_lon', 'datetime':'local_datetime'}, inplace=True)\n",
- " agent.sparse_traj.rename(columns={'uid': 'user_id'}, inplace=True)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "ed6116df",
- "metadata": {},
- "outputs": [],
- "source": [
- "agent_id, agent = [(agent_id, agent) for agent_id, agent in population.roster.items()][0]\n",
- "agent_id"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "9724ff27",
- "metadata": {},
- "outputs": [],
- "source": [
- "def generate_trajectory_data(agent_id, agent, seed):\n",
- " agent.reset_trajectory()\n",
+ "for i, (agent_id, agent) in enumerate(population.roster.items()):\n",
+ " ax_map = axes[0, i]\n",
+ " ax_barcode = axes[1, i]\n",
" \n",
- " agent.generate_trajectory(\n",
- " local_timestamp=\"2024-01-01T06:00:00 +02:00\",\n",
- " end_time=pd.Timestamp(\"2024-01-20T12:00:00 +02:00\"),\n",
- " seed=105,\n",
- " dt=1)\n",
- "\n",
- " beta_duration = npr.uniform(25, 170)\n",
- " beta_start = max(npr.uniform(25, 520), beta_duration)\n",
- " beta_ping = min(npr.uniform(3, 15), beta_duration//2)\n",
- "\n",
- " agent.sample_trajectory(\n",
- " beta_start=beta_start,\n",
- " beta_durations=beta_duration,\n",
- " beta_ping=beta_ping,\n",
- " seed=seed,\n",
- " replace_sparse_traj=True)\n",
- "\n",
- " garden_city_to_lat_long(agent, sparse_traj=True, full_traj=False)\n",
- " return None\n",
+ " city.plot_city(ax=ax_map, doors=False, address=False)\n",
+ " \n",
+ " traj = agent.sparse_traj\n",
+ " plot_pings(traj, ax=ax_map, s=15, point_color='red', \n",
+ " x='x', y='y', timestamp='timestamp')\n",
+ " \n",
+ " plot_time_barcode(traj['timestamp'], ax=ax_barcode, set_xlim=True)\n",
+ " \n",
+ " q = len(traj) / len(agent.trajectory)\n",
+ " ax_map.set_title(f\"Agent {i}: {len(traj)} obs (q={q:.2f})\\n\"\n",
+ " f\"beta_start={sampling_params[i]['beta_start']}, \"\n",
+ " f\"beta_dur={sampling_params[i]['beta_durations']}\")\n",
+ " ax_map.set_axis_off()\n",
"\n",
- "# Generate trajectories with progress bar\n",
- "for agent_id, agent in tqdm(population.roster.items(), desc=\"Generating trajectories\"):\n",
- " generate_trajectory_data(agent_id, agent, seed=150)"
+ "plt.tight_layout()\n",
+ "plt.savefig('data/trajectories_visualization.png', dpi=150, bbox_inches='tight')\n",
+ "plt.show()"
]
},
{
- "cell_type": "code",
- "execution_count": null,
- "id": "957367ef",
+ "cell_type": "markdown",
+ "id": "dc7b266a",
"metadata": {},
- "outputs": [],
"source": [
- "# dataset no 1\n",
- "traj_cols = {\n",
- " \"user_id\": \"identifier\",\n",
- " \"timestamp\": \"unix_timestamp\",\n",
- " \"latitude\": \"device_lat\",\n",
- " \"longitude\": \"device_lon\",\n",
- " \"datetime\": \"local_datetime\"}\n",
- "# Save only sparse trajectories and diaries\n",
- "population.save_pop(\n",
- " sparse_path=\"output/gc_data.csv\",\n",
- " diaries_path=None,\n",
- " partition_cols=None,\n",
- " traj_cols=traj_cols,\n",
- " fmt=\"csv\"\n",
- ")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "8b8b2f21",
- "metadata": {
- "tags": []
- },
- "outputs": [],
- "source": [
- "# dataset no 2\n",
- "traj_cols = {\n",
- " \"user_id\": \"user_id\",\n",
- " \"timestamp\": \"timestamp\",\n",
- " \"latitude\": \"latitude\",\n",
- " \"longitude\": \"longitude\",\n",
- " \"datetime\": \"datetime\"}\n",
- "# Save only sparse trajectories and diaries\n",
- "population.save_pop(\n",
- " sparse_path=\"output/gc_data/\",\n",
- " diaries_path=None,\n",
- " partition_cols=['date'],\n",
- " traj_cols=traj_cols,\n",
- " fmt=\"csv\"\n",
- ")"
+ "## Part 2: Parallel Generation at Scale\n",
+ "\n",
+ "Generate trajectories for 15 users using parallelization."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "3c870944",
+ "id": "5238c745",
"metadata": {
- "scrolled": true
+ "lines_to_next_cell": 1
},
"outputs": [],
"source": [
- "sparse_df = loader.from_file(\"output/gc_data/\", format=\"csv\", traj_cols=traj_cols,\n",
- " parse_dates=True)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "340dab62",
- "metadata": {},
- "source": [
- "## Generate dataset 3 for tutorial"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "1b22e370",
- "metadata": {},
- "outputs": [],
- "source": [
- "def generate_trajectory_data(agent, seed_trajectory=None, seed_sparsity=None, use_datetime=None, use_lon_lat=True):\n",
- " beta_params = gen_params_target_q(q_range=(0.2, 0.8), beta_dur_range=(25, 180), beta_ping_range=(1.5, 6), seed=seed_sparsity)\n",
- " rng = npr.default_rng(seed_sparsity)\n",
- " ha_sample = rng.uniform(11.5/15, 16.5/15)\n",
- "\n",
- " agent.reset_trajectory()\n",
+ "def generate_agent_trajectory(args):\n",
+ " \"\"\"Worker function for parallel generation.\"\"\"\n",
+ " identifier, home, work, seed = args\n",
+ " \n",
+ " city = City.from_geopackage('garden-city.gpkg', edges_path='garden-city-edges.parquet')\n",
+ " city._build_hub_network(hub_size=16)\n",
+ " city.compute_gravity(exponent=2.0)\n",
+ " agent = Agent(identifier=identifier, city=city, home=home, workplace=work)\n",
+ " \n",
" agent.generate_trajectory(\n",
- " datetime = \"2024-01-01T07:00 -04:00\",\n",
- " end_time = pd.Timestamp('2024-01-21T09:00:00 -04:00'),\n",
- " seed=seed_trajectory,\n",
- " dt=0.15)\n",
+ " datetime=pd.Timestamp(\"2024-01-01T07:00-04:00\"),\n",
+ " end_time=pd.Timestamp(\"2024-01-08T07:00-04:00\"),\n",
+ " seed=seed\n",
+ " )\n",
"\n",
" agent.sample_trajectory(\n",
- " **beta_params,\n",
- " seed=seed_sparsity,\n",
- " ha=ha_sample,\n",
- " replace_sparse_traj=True)\n",
- "\n",
- " garden_city_to_lat_long(agent, sparse_traj=True, full_traj=False, use_datetime=use_datetime, use_lon_lat=use_lon_lat)\n",
- " agent.reset_trajectory(trajectory = True, sparse = False, diary = False)\n",
- " return None\n",
- "\n",
- "# Generate trajectories with progress bar\n",
- "N = 350 \n",
- "population = Population(city)\n",
- "population.generate_agents(N=N, seed=5, name_count=2)\n",
- "\n",
- "for i, agent in enumerate(tqdm(population.roster.values(), desc=\"Generating trajectories\")):\n",
- " if i == 0:\n",
- " continue\n",
- " generate_trajectory_data(agent, seed_trajectory=i, seed_sparsity=i, use_datetime=False, use_lon_lat=False)\n",
- " agent.sparse_traj.rename(columns={'uid': 'gc_identifier', 'timestamp': 'unix_ts', 'x':'dev_x', 'y':'dev_y'}, inplace=True)"
+ " beta_ping=5,\n",
+ " replace_sparse_traj=True,\n",
+ " seed=seed\n",
+ " )\n",
+ " \n",
+ " sparse_df = agent.sparse_traj.copy()\n",
+ " sparse_df['user_id'] = identifier\n",
+ " return sparse_df"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "f71d0544",
+ "id": "aa1b6112",
"metadata": {},
"outputs": [],
"source": [
- "# Make data for agent 0\n",
- "start_time = pd.date_range(start='2024-01-01T07:00 -04:00', periods=4, freq='60min')\n",
- "tz_offset = loader._offset_seconds_from_ts(start_time[0])\n",
- "unix_timestamp = [int(t.timestamp()) for t in start_time]\n",
- "duration = [60]*4 # in minutes\n",
- "location = ['h-x13-y11'] * 1 + ['h-x13-y9'] * 1 + ['w-x18-y10'] * 1 + ['w-x18-y8'] * 1\n",
- "\n",
- "destinations = pd.DataFrame(\n",
- " {\"datetime\":start_time,\n",
- " \"timestamp\":unix_timestamp,\n",
- " \"duration\":duration,\n",
- " \"location\":location}\n",
- " )\n",
- "destinations = condense_destinations(destinations)\n",
- "\n",
- "agent_0 = list(population.roster.values())[0]\n",
- "\n",
- "rng = npr.default_rng(0)\n",
- "ha_sample = rng.uniform(11.5/15, 16.5/15)\n",
- "\n",
- "agent_0.reset_trajectory()\n",
- "agent_0.generate_trajectory(destination_diary=destinations, seed=0, dt=0.15)\n",
+ "np.random.seed(100)\n",
+ "n_agents = 15\n",
+ "homes = city.buildings_gdf[city.buildings_gdf['building_type'] == 'home']['id'].tolist()\n",
+ "workplaces = city.buildings_gdf[city.buildings_gdf['building_type'] == 'workplace']['id'].tolist()\n",
"\n",
- "agent_0.sample_trajectory(\n",
- " beta_ping=2,\n",
- " beta_start=None,\n",
- " beta_durations=None,\n",
- " seed=0,\n",
- " ha=ha_sample,\n",
- " replace_sparse_traj=True)\n",
- "\n",
- "garden_city_to_lat_long(agent_0, sparse_traj=True, full_traj=False, use_datetime=False, use_lon_lat=False)\n",
- "agent_0.reset_trajectory(trajectory = True, sparse = False, diary = False)\n",
- "agent_0.sparse_traj.rename(columns={'uid': 'gc_identifier', 'timestamp': 'unix_ts', 'x':'dev_x', 'y':'dev_y'}, inplace=True)"
+ "agent_params = [\n",
+ " (f'agent_{i:04d}', \n",
+ " np.random.choice(homes),\n",
+ " np.random.choice(workplaces),\n",
+ " i)\n",
+ " for i in range(n_agents)\n",
+ "]"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "aff38e9c",
+ "id": "a0364bc9",
"metadata": {},
"outputs": [],
"source": [
- "# dataset no 3\n",
- "traj_cols = {\n",
- " \"user_id\": \"gc_identifier\",\n",
- " \"timestamp\": \"unix_ts\",\n",
- " \"x\": \"dev_x\",\n",
- " \"y\": \"dev_y\"}\n",
- "# Save only sparse trajectories and diaries\n",
- "population.save_pop(\n",
- " sparse_path=\"output/gc_data_long/\",\n",
- " diaries_path=None,\n",
- " partition_cols=['date'],\n",
- " traj_cols=traj_cols,\n",
- " fmt=\"parquet\"\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "5c2af855",
- "metadata": {},
- "source": [
- "For larger trajectory generation tasks that require a lot of compute power, we can parallelize the trajectory generation using the following code. We generate ground-truth trajectories in agent-month \"chunks\", sparsify each chunk, then reset the ground-truth trajectory field to lessen the memory usage. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "41b6d757",
- "metadata": {
- "tags": []
- },
- "outputs": [],
- "source": [
- "# Using parallel processing (e.g., using a cluster)\n",
- "%%time\n",
+ "print(f\"Generating {n_agents} agents in parallel...\")\n",
+ "start_time = time.time()\n",
"\n",
- "def generate_agent_trajectory(agent_id, agent, seed):\n",
- " \n",
- " beta_duration = npr.uniform(15, 180)\n",
- " beta_start = max(npr.uniform(60, 1200), beta_duration*3)\n",
- " beta_ping = npr.uniform(1.5, 30)\n",
- " \n",
- " param = (beta_start, beta_duration, beta_ping)\n",
- " \n",
- " for month in range(1,13):\n",
- " days = calendar.monthrange(2024, month)[1]\n",
- " population_n.generate_trajectory(agent, \n",
- " T=datetime(2024, month, days, hour=23, minute=59), \n",
- " seed=seed)\n",
- " \n",
- " agent.sample_traj_hier_nhpp(*param, \n",
- " seed=seed,\n",
- " reset_traj=True)\n",
- " \n",
- " garden_city_to_lat_long(agent,\n",
- " sparse_traj=True,\n",
- " full_traj=False)\n",
- " \n",
- " return agent_id, copy.deepcopy(agent)\n",
- "\n",
- "manager = multiprocessing.Manager()\n",
- "shared_roster = manager.dict(population_n.roster)\n",
- "\n",
- "start = 6001 # 12001 # can modify\n",
- "end = 12001 # 18001 # can modify\n",
- "roster = dict(population_n.roster)\n",
- "batch = islice(roster.items(), start, end)\n",
- "\n",
- "with ProcessPoolExecutor() as executor:\n",
- " with tqdm(total=(end-start), desc=\"Processing agents\") as pbar:\n",
- " futures = [\n",
- " executor.submit(generate_agent_trajectory, agent_id, agent, i+15000)\n",
- " for i, (agent_id, agent) in enumerate(batch, start=start)\n",
- " ]\n",
- " results = []\n",
- " for future in futures:\n",
- " results.append(future.result())\n",
- " pbar.update(1)\n",
+ "results = Parallel(n_jobs=-1, verbose=10)(\n",
+ " delayed(generate_agent_trajectory)(params) for params in agent_params\n",
+ ")\n",
"\n",
- "for agent_id, agent in results:\n",
- " population_n.roster[agent_id] = agent"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "68ea90d9",
- "metadata": {},
- "source": [
- "This code saves the generated trajectories in a parquet file, using the date as the partition column. "
+ "generation_time = time.time() - start_time\n",
+ "print(f\"Generated {n_agents} agents in {generation_time:.2f}s ({generation_time/n_agents:.2f}s per agent)\")"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "a8d9885f",
- "metadata": {
- "tags": []
- },
+ "id": "e8a48448",
+ "metadata": {},
"outputs": [],
"source": [
- "partition_cols = {\n",
- " 'sparse_traj': ['date'],\n",
- " 'diaries': ['identifier']\n",
- "}\n",
+ "all_trajectories = pd.concat(results, ignore_index=True)\n",
+ "all_trajectories = city.to_mercator(all_trajectories)\n",
+ "all_trajectories['date'] = pd.to_datetime(all_trajectories['datetime']).dt.date\n",
"\n",
- "roster = dict(islice(population_n.roster.items(), start, end))\n",
+ "output_path = 'data/trajectories_15_users'\n",
+ "for date, group in all_trajectories.groupby('date'):\n",
+ " os.makedirs(f'{output_path}/date={str(date)}', exist_ok=True)\n",
+ " group.to_parquet(f'{output_path}/date={str(date)}/data.parquet', index=False)\n",
"\n",
- "population.save_pop(bucket=\"synthetic-raw-data\",\n",
- " prefix=f\"agents-{start+15000}-{end+15000-1}/\",\n",
- " save_full_traj=False,\n",
- " save_sparse_traj=True,\n",
- " save_homes=True,\n",
- " save_diaries=True,\n",
- " partition_cols=partition_cols,\n",
- " roster=roster)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "bee94160",
- "metadata": {
- "tags": []
- },
- "outputs": [],
- "source": [
- "# Load the Parquet files\n",
- "s3_path = \"s3://synthetic-raw-data/agents-1-1001/sparse_trajectories.parquet/\"\n",
- "df1 = pd.read_parquet(s3_path)\n",
- "s3_path = \"s3://synthetic-raw-data/agents-1001-2000/sparse_trajectories.parquet/\"\n",
- "df2 = pd.read_parquet(s3_path)"
+ "print(f\"Saved {len(all_trajectories):,} records to {output_path}/\")"
]
}
],
"metadata": {
"jupytext": {
- "formats": "ipynb,auto:percent"
+ "formats": "ipynb,py:percent"
},
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.10.0"
- },
- "nbdime-conflicts": {
- "local_diff": [
- {
- "diff": [
- {
- "diff": [
- {
- "key": 0,
- "op": "addrange",
- "valuelist": [
- "Python 3 (ipykernel)"
- ]
- },
- {
- "key": 0,
- "length": 1,
- "op": "removerange"
- }
- ],
- "key": "display_name",
- "op": "patch"
- }
- ],
- "key": "kernelspec",
- "op": "patch"
- },
- {
- "diff": [
- {
- "diff": [
- {
- "key": 0,
- "length": 1,
- "op": "removerange"
- }
- ],
- "key": "version",
- "op": "patch"
- }
- ],
- "key": "language_info",
- "op": "patch"
- }
- ],
- "remote_diff": [
- {
- "diff": [
- {
- "diff": [
- {
- "key": 0,
- "op": "addrange",
- "valuelist": [
- "Python 3.10 (daphme)"
- ]
- },
- {
- "key": 0,
- "length": 1,
- "op": "removerange"
- }
- ],
- "key": "display_name",
- "op": "patch"
- }
- ],
- "key": "kernelspec",
- "op": "patch"
- },
- {
- "diff": [
- {
- "diff": [
- {
- "diff": [
- {
- "key": 3,
- "op": "addrange",
- "valuelist": "0"
- },
- {
- "key": 3,
- "length": 1,
- "op": "removerange"
- }
- ],
- "key": 0,
- "op": "patch"
- }
- ],
- "key": "version",
- "op": "patch"
- }
- ],
- "key": "language_info",
- "op": "patch"
- }
- ]
- },
- "toc-autonumbering": false
+ }
},
"nbformat": 4,
"nbformat_minor": 5
diff --git a/examples/grid_based_demo.ipynb b/examples/grid_based_demo.ipynb
index 03022d0a..90114911 100644
--- a/examples/grid_based_demo.ipynb
+++ b/examples/grid_based_demo.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
- "id": "92838936",
+ "id": "f56b531d",
"metadata": {},
"source": [
"# Grid-Based Stop Detection"
@@ -10,7 +10,7 @@
},
{
"cell_type": "markdown",
- "id": "cb276fd9",
+ "id": "9cfdb26e",
"metadata": {},
"source": [
"The stop detection algorithms implemented in `nomad` support different combinations of input formats that are common in commercial datasets, detecting default names when possible\n",
@@ -23,69 +23,78 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "19184dee",
- "metadata": {},
+ "execution_count": 1,
+ "id": "24b50a14",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:11.959806Z",
+ "iopub.status.busy": "2025-11-24T18:32:11.959806Z",
+ "iopub.status.idle": "2025-11-24T18:32:16.169725Z",
+ "shell.execute_reply": "2025-11-24T18:32:16.169725Z"
+ }
+ },
"outputs": [],
"source": [
"%matplotlib inline\n",
+ "import matplotlib\n",
+ "matplotlib.use('TkAgg') # Non-blocking backend\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.ion() # Interactive mode\n",
"\n",
"# Imports\n",
"import nomad.io.base as loader\n",
"import geopandas as gpd\n",
"from shapely.geometry import box\n",
- "import pandas as pd\n",
- "import numpy as np\n",
- "import matplotlib.pyplot as plt\n",
- "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode\n",
+ "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_hexagons, plot_pings\n",
"import nomad.stop_detection.grid_based as GRID_BASED\n",
"import nomad.filters as filters \n",
"\n",
"# Load data\n",
- "city = gpd.read_file(\"garden_city.geojson\").to_crs('EPSG:3857')\n",
- "outer_box = box(*city.total_bounds).buffer(15, join_style='mitre')\n",
+ "import nomad.data as data_folder\n",
+ "from pathlib import Path\n",
+ "data_dir = Path(data_folder.__file__).parent\n",
+ "city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')\n",
+ "outer_box = box(*city.total_bounds)\n",
"\n",
- "filepath_root = '../tutorials/IC2S2-2025/gc_data_long/'\n",
- "tc = {\n",
- " \"user_id\": \"gc_identifier\",\n",
- " \"timestamp\": \"unix_ts\",\n",
- " \"x\": \"dev_x\",\n",
- " \"y\": \"dev_y\",\n",
- " \"ha\":\"ha\",\n",
- " \"date\":\"date\"}\n",
+ "filepath_root = 'gc_data_long/'\n",
+ "tc = {\"user_id\": \"gc_identifier\", \"x\": \"dev_x\", \"y\": \"dev_y\", \"timestamp\": \"unix_ts\"}\n",
"\n",
"users = ['admiring_brattain']\n",
- "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','==', '2024-01-01'), traj_cols=tc)\n",
+ "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)\n",
"\n",
- "# Grid-based\n",
- "traj['h3_cell'] = filters.to_tessellation(traj, index=\"h3\", res=10, x='dev_x', y='dev_y', data_crs='EPSG:3857')\n",
- "stops_gb = GRID_BASED.grid_based(traj, time_thresh=240, complete_output=True, timestamp='unix_ts', location_id='h3_cell')"
+ "# Grid-based - data is in Web Mercator (EPSG:3857) projected coordinates\n",
+ "traj['h3_cell'] = filters.to_tessellation(traj, index=\"h3\", res=10, traj_cols=tc, data_crs='EPSG:3857')\n",
+ "stops_gb = GRID_BASED.grid_based(traj, time_thresh=240, complete_output=True, location_id='h3_cell', traj_cols=tc)"
]
},
{
"cell_type": "code",
- "execution_count": 21,
- "id": "62555a1b",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAACMCAYAAABh9MpJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAf0UlEQVR4nO3deXRU9f3/8dckk8m+sCSWyFYwrCL8vojSooKagobFVhFMwUKpHkQE7TmtonXDys+D39qjglCkgFplEUQPxpZzsA0uHJUlRI9VItqEqoAQCFkJ2T6/P/jd68xkJpkslwzx+ThnDsy9n/v5vO/nfu7n3ndmcxljjAAAAAAAQLuL6OgAAAAAAADorEi6AQAAAABwCEk3AAAAAAAOIekGAAAAAMAhJN0AAAAAADiEpBsAAAAAAIeQdAMAAAAA4BCSbgAAAAAAHELSDQAAAACAQ0i6AQA/GLNnz1bfvn2bLVdUVCSXy6UXXnjB8Zg6St++fTV79uyODgMAgE6PpBsAEPYKCwt11113acCAAYqLi1NcXJyGDBmi+fPn65NPPumwuHbu3CmXy+Xz6Nq1q0aPHq1XXnmlw+I6lz777DM9+uijKioq6uhQAAAIS+6ODgAAgKbk5ORo+vTpcrvdmjFjhoYPH66IiAgdOHBAW7du1cqVK1VYWKg+ffo0W9fq1avV0NDQ7jEuXLhQo0aNkiSdOHFCmzZt0syZM3Xq1CnNnz+/3dsLJ5999pkWL16scePGhfQuAgAAfmhIugEAYeurr77SLbfcoj59+uif//ynevTo4bN+6dKlWrFihSIimn7jVmVlpeLj4xUVFeVInFdeeaWmTp1qP583b5769eun9evXd/qkGwAANI23lwMAwtaTTz6pyspKrVu3rlHCLUlut1sLFy5Ur1697GWzZ89WQkKCvvrqK2VlZSkxMVEzZsyw1/m/Gnvq1CnNnj1bycnJSklJ0axZs3Tq1Kk2xe3xeNSlSxe53b5/2163bp2uueYapaWlKTo6WkOGDNHKlSsbbb93715NmDBB3bt3V2xsrH784x9rzpw5PmUaGhr09NNPa+jQoYqJidEFF1yguXPnqqSkxKecMUaPP/64evbsqbi4OF199dX697//HfK+bNy4USNHjlRiYqKSkpI0bNgwPfPMM5KkF154QTfffLMk6eqrr7bfYr9z5057+xUrVmjo0KGKjo5Wenq65s+f36h/x40bp4svvlj79u3TT3/6U3uf//KXvzSKZ9myZRo6dKji4uLUpUsXXXrppVq/fn3I+wMAwLnGK90AgLCVk5Ojiy66SJdffnmLtqurq9OECRN0xRVX6E9/+pPi4uICljPG6IYbbtD777+vO+64Q4MHD9brr7+uWbNmtai98vJyFRcXS5JOnjyp9evX69NPP9WaNWt8yq1cuVJDhw7VlClT5Ha79eabb+rOO+9UQ0OD/Yr4sWPHNH78eKWmpmrRokVKSUlRUVGRtm7d6lPX3Llz9cILL+jXv/61Fi5cqMLCQi1fvlz79+/Xrl277Ff1H374YT3++OPKyspSVlaW8vLyNH78eNXU1DS7Xzt27FB2drauvfZaLV26VJL0+eefa9euXbr77rt11VVXaeHChXr22Wf1wAMPaPDgwZJk//voo49q8eLFyszM1Lx581RQUKCVK1dqz549PjFKUklJibKysjRt2jRlZ2fr1Vdf1bx58+TxeOw/OKxevVoLFy7U1KlTdffdd6u6ulqffPKJPvroI/3yl79s0TEDAOCcMQAAhKHS0lIjyfz85z9vtK6kpMQcP37cflRVVdnrZs2aZSSZRYsWNdpu1qxZpk+fPvbzN954w0gyTz75pL2srq7OXHnllUaSWbduXZMx5ubmGkmNHhEREWbJkiWNynvHaZkwYYLp16+f/fz11183ksyePXuCtvvee+8ZSeaVV17xWb59+3af5ceOHTMej8dMnDjRNDQ02OUeeOABI8nMmjWryf27++67TVJSkqmrqwtaZvPmzUaSyc3N9VlutT1+/HhTX19vL1++fLmRZNauXWsvGzt2rJFknnrqKXvZmTNnzIgRI0xaWpqpqakxxhhzww03mKFDhzYZMwAA4Ya3lwMAwlJZWZkkKSEhodG6cePGKTU11X4899xzjcrMmzev2Tb+/ve/y+12+5SNjIzUggULWhTrww8/rB07dmjHjh3atGmTsrOz9Yc//MF+G7YlNjbW/n9paamKi4s1duxY/ec//1FpaakkKSUlRdLZV/lra2sDtrd582YlJyfrZz/7mYqLi+3HyJEjlZCQoNzcXEnS22+/rZqaGi1YsEAul8ve/p577glpv1JSUlRZWakdO3aE2hU2q+177rnH5zP3t99+u5KSkvTWW2/5lHe73Zo7d6793OPxaO7cuTp27Jj27dtnx/PNN99oz549LY4HAICOQtINAAhLiYmJkqSKiopG61atWqUdO3bo5ZdfDrit2+1Wz549m23j0KFD6tGjR6PEfuDAgT7PT58+raNHj/o8vA0bNkyZmZnKzMzUtGnT9PLLL2vSpElatGiRjh8/bpfbtWuXMjMzFR8fr5SUFKWmpuqBBx6QJDvpHjt2rG666SYtXrxY3bt31w033KB169bpzJkzdj0HDx5UaWmp0tLSfP74kJqaqoqKCh07dszeP0nKyMjwiTc1NVVdunRptn/uvPNODRgwQNdff7169uypOXPmaPv27c1u5922f196PB7169fPXm9JT09XfHy8z7IBAwZIkv1zZPfdd58SEhJ02WWXKSMjQ/Pnz9euXbtCigcAgI5C0g0ACEvJycnq0aOHPv3000brLr/8cmVmZmrMmDEBt42Ojm72G81bYtOmTerRo4fPoznXXnutqqurtXv3bklnv4n92muvVXFxsf785z/rrbfe0o4dO/Tb3/5WkuyfMnO5XNqyZYs++OAD3XXXXfr22281Z84cjRw50v4DRENDg9LS0uxX1/0fjz32WLvsd1pamvLz87Vt2zZNmTJFubm5uv7661v8mff2MnjwYBUUFGjjxo264oor9Nprr+mKK67QI4880iHxAAAQCr5IDQAQtiZOnKi//vWv2r17ty677LJ2r9/6KbKKigqfV7sLCgp8yk2YMKHFb7Guq6uT9P0r9W+++abOnDmjbdu2qXfv3nY5663g/kaPHq3Ro0dryZIlWr9+vWbMmKGNGzfqtttuU//+/fX2229rzJgxPm9ZD7R/0tlXxvv162cvP378eKNvOQ/G4/Fo8uTJmjx5shoaGnTnnXdq1apVeuihh3TRRRf5vG09UNsFBQU+bdfU1KiwsFCZmZk+5Q8fPmz/tJvliy++kCSfb5yPj4/X9OnTNX36dNXU1OjGG2/UkiVLdP/99ysmJiakfQIA4FzilW4AQNi69957FRcXpzlz5ui7775rtN4Y06b6s7KyVFdX5/OzXfX19Vq2bJlPuR49ethvH7cezcnJyZEkDR8+XNLZz4r7x1xaWqp169b5bFdSUtJov0aMGCFJ9lvMp02bpvr6ev3xj39s1G5dXZ39k1yZmZmKiorSsmXLfOp8+umnm41fkk6cOOHzPCIiQpdccolPLFaS7P8zYJmZ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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "execution_count": 2,
+ "id": "4fea8a03",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:16.169725Z",
+ "iopub.status.busy": "2025-11-24T18:32:16.169725Z",
+ "iopub.status.idle": "2025-11-24T18:32:16.492839Z",
+ "shell.execute_reply": "2025-11-24T18:32:16.492839Z"
}
- ],
+ },
+ "outputs": [],
"source": [
- "fig, ax_barcode = plt.subplots(figsize=(10,1.5))\n",
+ "fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),\n",
+ " gridspec_kw={'height_ratios':[10,1]})\n",
+ "\n",
+ "gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')\n",
+ "city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')\n",
+ "\n",
+ "plot_hexagons(stops_gb, ax=ax_map, color='cluster', cmap='Greens', location_id='h3_cell', data_crs='EPSG:3857')\n",
+ "plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)\n",
+ "ax_map.set_axis_off()\n",
"\n",
"plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops_gb, ax=ax_barcode, stop_color='green', set_xlim=False, timestamp='unix_ts')\n",
- "plt.title(\"Grid-Based stops\")\n",
- "plt.tight_layout()\n",
+ "plot_stops_barcode(stops_gb, ax=ax_barcode, cmap='Greens', set_xlim=False, timestamp='unix_ts')\n",
+ "\n",
+ "plt.tight_layout(pad=0.1)\n",
"plt.show()"
]
}
@@ -106,7 +115,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.14.0"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/examples/grid_based_demo.py b/examples/grid_based_demo.py
index 7ec92360..4d667bbd 100644
--- a/examples/grid_based_demo.py
+++ b/examples/grid_based_demo.py
@@ -25,38 +25,49 @@
# %%
# %matplotlib inline
+import matplotlib
+matplotlib.use('TkAgg') # Non-blocking backend
+import matplotlib.pyplot as plt
+plt.ion() # Interactive mode
# Imports
import nomad.io.base as loader
import geopandas as gpd
from shapely.geometry import box
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode
+from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_hexagons, plot_pings
import nomad.stop_detection.grid_based as GRID_BASED
import nomad.filters as filters
# Load data
-from nomad.city_gen import City
-city_obj = City.from_geopackage("garden-city.gpkg")
-outer_box = box(0, 0, city_obj.dimensions[0], city_obj.dimensions[1])
+import nomad.data as data_folder
+from pathlib import Path
+data_dir = Path(data_folder.__file__).parent
+city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')
+outer_box = box(*city.total_bounds)
filepath_root = 'gc_data_long/'
tc = {"user_id": "gc_identifier", "x": "dev_x", "y": "dev_y", "timestamp": "unix_ts"}
users = ['admiring_brattain']
-traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','==', '2024-01-01'), traj_cols=tc)
+traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)
# Grid-based - data is in Web Mercator (EPSG:3857) projected coordinates
traj['h3_cell'] = filters.to_tessellation(traj, index="h3", res=10, traj_cols=tc, data_crs='EPSG:3857')
stops_gb = GRID_BASED.grid_based(traj, time_thresh=240, complete_output=True, location_id='h3_cell', traj_cols=tc)
# %%
-fig, ax_barcode = plt.subplots(figsize=(10,1.5))
+fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),
+ gridspec_kw={'height_ratios':[10,1]})
+
+gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')
+city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')
+
+plot_hexagons(stops_gb, ax=ax_map, color='cluster', cmap='Greens', location_id='h3_cell', data_crs='EPSG:3857')
+plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)
+ax_map.set_axis_off()
plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)
-plot_stops_barcode(stops_gb, ax=ax_barcode, stop_color='green', set_xlim=False, timestamp='unix_ts')
-plt.title("Grid-Based stops")
-plt.tight_layout()
+plot_stops_barcode(stops_gb, ax=ax_barcode, cmap='Greens', set_xlim=False, timestamp='unix_ts')
+
+plt.tight_layout(pad=0.1)
plt.show()
diff --git a/examples/hdbscan_demo.ipynb b/examples/hdbscan_demo.ipynb
index 4437d09a..b7c6c4e4 100644
--- a/examples/hdbscan_demo.ipynb
+++ b/examples/hdbscan_demo.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
- "id": "92838936",
+ "id": "4f16fcbe",
"metadata": {},
"source": [
"# HDBSCAN Stop Detection"
@@ -10,7 +10,7 @@
},
{
"cell_type": "markdown",
- "id": "cb276fd9",
+ "id": "f69ed80f",
"metadata": {},
"source": [
"The HDBSCAN algorithm constructs a hierarchy of non-overlapping clusters from different radius values and selects those that maximize stability."
@@ -18,73 +18,79 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "19184dee",
- "metadata": {},
+ "execution_count": 1,
+ "id": "3561532d",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:30.624504Z",
+ "iopub.status.busy": "2025-11-24T18:32:30.624504Z",
+ "iopub.status.idle": "2025-11-24T18:32:33.741073Z",
+ "shell.execute_reply": "2025-11-24T18:32:33.740043Z"
+ }
+ },
"outputs": [],
"source": [
"%matplotlib inline\n",
+ "import matplotlib\n",
+ "matplotlib.use('TkAgg')\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.ion()\n",
"\n",
"# Imports\n",
"import nomad.io.base as loader\n",
"import geopandas as gpd\n",
"from shapely.geometry import box\n",
- "import pandas as pd\n",
- "import numpy as np\n",
- "import matplotlib.pyplot as plt\n",
- "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode\n",
+ "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_stops, plot_pings\n",
"import nomad.stop_detection.hdbscan as HDBSCAN\n",
- "import nomad.stop_detection.postprocessing as post\n",
"\n",
"# Load data\n",
- "city = gpd.read_file(\"garden_city.geojson\").to_crs('EPSG:3857')\n",
- "outer_box = box(*city.total_bounds).buffer(15, join_style='mitre')\n",
+ "import nomad.data as data_folder\n",
+ "from pathlib import Path\n",
+ "data_dir = Path(data_folder.__file__).parent\n",
+ "city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')\n",
+ "outer_box = box(*city.total_bounds)\n",
"\n",
"filepath_root = 'gc_data_long/'\n",
- "tc = {\n",
- " \"user_id\": \"gc_identifier\",\n",
- " \"timestamp\": \"unix_ts\",\n",
- " \"x\": \"dev_x\",\n",
- " \"y\": \"dev_y\",\n",
- " \"ha\":\"ha\",\n",
- " \"date\":\"date\"}\n",
+ "tc = {\"user_id\": \"gc_identifier\", \"x\": \"dev_x\", \"y\": \"dev_y\", \"timestamp\": \"unix_ts\"}\n",
"\n",
"users = ['admiring_brattain']\n",
- "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','==', '2024-01-01'), traj_cols=tc)\n",
+ "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)\n",
"\n",
- "user_data_hdb = traj.assign(cluster=HDBSCAN.hdbscan_labels(traj, time_thresh=240, min_pts=3, min_cluster_size=2, traj_cols=tc))\n",
"stops_hdb = HDBSCAN.st_hdbscan(traj,\n",
" time_thresh=720,\n",
- " dist_thresh=15,\n",
" min_pts=3,\n",
" complete_output=True,\n",
- " traj_cols=tc)\n",
- "stops_hdb[\"cluster\"] = post.remove_overlaps(user_data_hdb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3) "
+ " traj_cols=tc) "
]
},
{
"cell_type": "code",
- "execution_count": 30,
- "id": "fa70719e",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxoAAACuCAYAAACx83usAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAm8klEQVR4nO3deXgV9b3H8c8hZCMbEAgQE8ImBCKLhYKCNMQAslMBQ0B2awsUBaUqyhYoiiC3LsXYp/ey+RhZvKKyKJWYsJXlgkChorgQCCk7GghgzPa7f9BzyuGc5CRhJBDer+fJI+d3fjPzm5nvjPlk5syxGWOMAAAAAMBCVSp6AAAAAAAqH4IGAAAAAMsRNAAAAABYjqABAAAAwHIEDQAAAACWI2gAAAAAsBxBAwAAAIDlCBoAAAAALEfQAAAAAGA5ggZwi1q6dKlsNpv27Nnj9v0+ffqoQYMGTm0NGjSQzWaTzWZTlSpVFBISoubNm2vEiBH69NNP3c7H3t/+ExAQoObNm2vWrFm6fPmyU9/Lly9r3rx5at26tYKDgxUUFKTGjRsrISFBmzdvdpn3kSNHNGHCBDVt2lT+/v6qVq2aYmJiNG3aNP3rX/9yO54BAwbIZrNpwoQJbt/ftGmTY6w7duxweX/UqFEKDAx0O21ZJScna+nSpZbM61bTpUsXdenSxfH6ypUrSkpK0qZNm1z6JiUlyWaz6dy5czdvgDfoxIkTSkpK0v79+yt6KLcN+7HlrgYAoDyqVvQAAFirU6dOWrBggSTp0qVLOnz4sFasWKGHHnpIAwcO1PLly+Xt7e00zaBBgzR58mTHNJs3b9bs2bN14MABvf/++5KkwsJCde/eXQcPHtQzzzyj9u3bS5K++eYbrV27Vlu3blVsbKxjnuvWrVNiYqJq1aqlCRMm6N5775XNZtPBgwe1ePFirV+/Xvv27XMax5kzZ7Ru3TpJUkpKihYsWCA/P79i1/XZZ5/V1q1bb3CLFS85OVm1atXSqFGjfrZlVJTk5GSn11euXNGsWbMkySmA3K5OnDihWbNmqUGDBmrTpk1FD+e28Itf/EI7duxQixYtKnooACoJggZQyVSvXl333Xef43XXrl31+9//XklJSZo1a5amTZumefPmOU1Tp04dl2mOHTumlJQU5ebmys/PT1u2bNH27du1ePFijR492tH3oYce0oQJE1RUVORoy8jIUGJiopo2bar09HSFhIQ43nvwwQf15JNP6oMPPnAZ+9tvv638/Hz17t1b69ev1+rVqzV06FC369mjRw9t2LBBa9euVd++fcu+oe5w/DJpvR9//FH+/v4VPYxyCw4OdjoPAMCN4tYp4A6RlJSkmJgYLVy4ULm5uR77h4SEyGazycvLS5J0/vx5SVK9evXc9q9S5T+nkz/96U+6fPmykpOTnUKGnc1m04ABA1zaFy9erDp16mjZsmXy9/fX4sWLix3fqFGj1KJFCz3//PMqLCz0uD7XO3LkiBITExUeHi5fX1/VqVNH8fHxjlttGjRooC+++EKbN2923Kp17a1qmZmZGjZsmMLCwuTr66vmzZvrv/7rv5wC19GjR2Wz2TR//ny9+OKLql+/vvz8/NSuXTt99tlnTuM5e/asfvvb3yoyMlK+vr6qXbu2OnXqpNTU1GLX4YsvvpDNZtN7773naPv8889ls9kUExPj1Ldfv35q27at4/W1t04dPXpUtWvXliTNmjXLsb7XX8k5ffq0hgwZopCQENWpU0djxozRhQsXPG7rLl266J577tHWrVt13333yd/fX3fddZemT5/usu++//57jR8/XnfddZd8fHzUqFEjTZ06VT/99JNTv/fee08dOnRQSEiIqlWrpkaNGmnMmDGSrt4C9Mtf/lKSNHr0aMf6JCUllThO+y1i+/bt04ABAxQcHKyQkBANGzZMZ8+ederboEED9enTR6tXr9a9994rPz8/xxWhf/7zn+rfv79q1KghPz8/tWnTRsuWLXNZXnZ2tiZPnqxGjRrJ19dXYWFh6tWrl7766itHn7y8PM2ZM0fR0dGOuhg9erTLeNLS0tSlSxeFhobK399f9evX18CBA3XlyhVHn7feekutW7dWYGCggoKCFB0drRdeeMHxvrtbp+y3In777bfq1auXAgMDFRkZqcmTJ7vsk6ysLA0aNEhBQUGqXr26Hn30Ue3evVs2m63S3oIIoGRc0QBucYWFhSooKHBpN8aUeV59+/bVyy+/rD179uiBBx5wmpd9GfZbp5YtW6bExETHbVbt2rWTt7e3Jk6cqBkzZujBBx8sNnR8+umnLldJPNm+fbu+/PJLPfPMMwoNDdXAgQOVkpKijIwMNWzY0KW/l5eX5s6dq/79+2vZsmWOXzJLq1evXiosLNT8+fNVv359nTt3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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "execution_count": 2,
+ "id": "ca45c6c3",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:33.744689Z",
+ "iopub.status.busy": "2025-11-24T18:32:33.743614Z",
+ "iopub.status.idle": "2025-11-24T18:32:34.044704Z",
+ "shell.execute_reply": "2025-11-24T18:32:34.044704Z"
}
- ],
+ },
+ "outputs": [],
"source": [
- "fig, ax_barcode = plt.subplots(figsize=(10,1.5))\n",
+ "fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),\n",
+ " gridspec_kw={'height_ratios':[10,1]})\n",
+ "\n",
+ "gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')\n",
+ "city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')\n",
+ "\n",
+ "plot_stops(stops_hdb, ax=ax_map, cmap='Blues')\n",
+ "plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)\n",
+ "ax_map.set_axis_off()\n",
"\n",
"plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops_hdb, ax=ax_barcode, stop_color='blue', set_xlim=False, timestamp='unix_ts')\n",
- "fig.suptitle(\"HDBSCAN stops with post-processing\")\n",
+ "plot_stops_barcode(stops_hdb, ax=ax_barcode, cmap='Blues', set_xlim=False, timestamp='unix_ts')\n",
+ "\n",
+ "plt.tight_layout(pad=0.1)\n",
"plt.show()"
]
}
@@ -105,7 +111,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.5"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/examples/hdbscan_demo.py b/examples/hdbscan_demo.py
index 6eb922a1..fcef411f 100644
--- a/examples/hdbscan_demo.py
+++ b/examples/hdbscan_demo.py
@@ -20,41 +20,50 @@
# %%
# %matplotlib inline
+import matplotlib
+matplotlib.use('TkAgg')
+import matplotlib.pyplot as plt
+plt.ion()
# Imports
import nomad.io.base as loader
import geopandas as gpd
from shapely.geometry import box
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode
+from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_stops, plot_pings
import nomad.stop_detection.hdbscan as HDBSCAN
-import nomad.stop_detection.postprocessing as post
# Load data
-from nomad.city_gen import City
-city_obj = City.from_geopackage("garden-city.gpkg")
-outer_box = box(0, 0, city_obj.dimensions[0], city_obj.dimensions[1])
+import nomad.data as data_folder
+from pathlib import Path
+data_dir = Path(data_folder.__file__).parent
+city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')
+outer_box = box(*city.total_bounds)
filepath_root = 'gc_data_long/'
tc = {"user_id": "gc_identifier", "x": "dev_x", "y": "dev_y", "timestamp": "unix_ts"}
users = ['admiring_brattain']
-traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','==', '2024-01-01'), traj_cols=tc)
+traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)
-user_data_hdb = traj.assign(cluster=HDBSCAN.hdbscan_labels(traj, time_thresh=240, min_pts=3, min_cluster_size=2, traj_cols=tc))
stops_hdb = HDBSCAN.st_hdbscan(traj,
time_thresh=720,
min_pts=3,
complete_output=True,
- traj_cols=tc)
-stops_hdb["cluster"] = post.remove_overlaps(user_data_hdb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)
+ traj_cols=tc)
# %%
-fig, ax_barcode = plt.subplots(figsize=(10,1.5))
+fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),
+ gridspec_kw={'height_ratios':[10,1]})
+
+gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')
+city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')
+
+plot_stops(stops_hdb, ax=ax_map, cmap='Blues')
+plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)
+ax_map.set_axis_off()
plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)
-plot_stops_barcode(stops_hdb, ax=ax_barcode, stop_color='blue', set_xlim=False, timestamp='unix_ts')
-fig.suptitle("HDBSCAN stops with post-processing")
+plot_stops_barcode(stops_hdb, ax=ax_barcode, cmap='Blues', set_xlim=False, timestamp='unix_ts')
+
+plt.tight_layout(pad=0.1)
plt.show()
diff --git a/examples/ingesting-data.ipynb b/examples/ingesting-data.ipynb
new file mode 100644
index 00000000..ecee86c5
--- /dev/null
+++ b/examples/ingesting-data.ipynb
@@ -0,0 +1,139 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "18b18c53",
+ "metadata": {
+ "id": "460ff464-7812-41fb-bc5b-bc4f24e16499"
+ },
+ "source": [
+ "# Loading Trajectory Data\n",
+ "\n",
+ "Mobility data comes in many formats: timestamps as unix integers or ISO strings (with timezones), \n",
+ "coordinates in lat/lon or projected, files as single CSVs or partitioned directories.\n",
+ "\n",
+ "`nomad.io.from_file` handles these cases with a single function call."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e3245095",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import glob\n",
+ "import pandas as pd\n",
+ "import nomad.io.base as loader\n",
+ "import nomad.data as data_folder\n",
+ "from pathlib import Path\n",
+ "\n",
+ "data_dir = Path(data_folder.__file__).parent"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a48b37e7",
+ "metadata": {},
+ "source": [
+ "## Pandas vs nomad.io for partitioned data\n",
+ "\n",
+ "Partitioned directories (e.g., `date=2024-01-01/`, `date=2024-01-02/`, ...) require a loop with pandas:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0a3a2a1b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "csv_files = glob.glob(str(data_dir / \"partitioned_csv\" / \"*\" / \"*.csv\"))\n",
+ "df_list = []\n",
+ "for f in csv_files:\n",
+ " df_list.append(pd.read_csv(f))\n",
+ "df_pandas = pd.concat(df_list, ignore_index=True)\n",
+ "\n",
+ "print(f\"Pandas: {len(df_pandas)} rows\")\n",
+ "print(df_pandas.dtypes)\n",
+ "print(\"\\nFirst few rows:\")\n",
+ "print(df_pandas.head(3))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "71eb4656",
+ "metadata": {},
+ "source": [
+ "`nomad.io.from_file` handles partitioned directories in one line, plus automatic type casting and column mapping:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d48bf128",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "traj_cols = {\"user_id\": \"user_id\",\n",
+ " \"latitude\": \"dev_lat\",\n",
+ " \"longitude\": \"dev_lon\",\n",
+ " \"datetime\": \"local_datetime\"}\n",
+ "\n",
+ "df = loader.from_file(data_dir / \"partitioned_csv\", format=\"csv\", traj_cols=traj_cols, parse_dates=True)\n",
+ "print(f\"nomad.io: {len(df)} rows\")\n",
+ "print(df.dtypes)\n",
+ "print(\"\\nFirst few rows:\")\n",
+ "print(df.head(3))\n",
+ "print(\"\\nNote: 'local_datetime' is now datetime64[ns], not object!\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "20c187a2",
+ "metadata": {},
+ "source": [
+ "The same pattern works for Parquet files, with the type casting and processing relying on passing to the functions which columns correspond to the default \"typical\" spatio-temporal column names"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4ca035ae",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "traj_cols = {\"user_id\": \"uid\", \"timestamp\": \"timestamp\", \n",
+ " \"latitude\": \"latitude\", \"longitude\": \"longitude\", \"date\": \"date\"}\n",
+ "\n",
+ "df = loader.from_file(data_dir / \"partitioned_parquet\", format=\"parquet\", traj_cols=traj_cols, parse_dates=True)\n",
+ "print(f\"Loaded {len(df)} rows\")\n",
+ "print(df.dtypes)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1e2f4479",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# These are the default canonical columnn names\n",
+ "from nomad.constants import DEFAULT_SCHEMA\n",
+ "print(DEFAULT_SCHEMA.keys())"
+ ]
+ }
+ ],
+ "metadata": {
+ "jupytext": {
+ "formats": "ipynb,py:percent"
+ },
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/ingesting-data.py b/examples/ingesting-data.py
new file mode 100644
index 00000000..4cc46fd9
--- /dev/null
+++ b/examples/ingesting-data.py
@@ -0,0 +1,80 @@
+# ---
+# jupyter:
+# jupytext:
+# formats: ipynb,py:percent
+# text_representation:
+# extension: .py
+# format_name: percent
+# format_version: '1.3'
+# jupytext_version: 1.17.3
+# kernelspec:
+# display_name: Python 3 (ipykernel)
+# language: python
+# name: python3
+# ---
+
+# %% [markdown] id="460ff464-7812-41fb-bc5b-bc4f24e16499"
+# # Loading Trajectory Data
+#
+# Mobility data comes in many formats: timestamps as unix integers or ISO strings (with timezones),
+# coordinates in lat/lon or projected, files as single CSVs or partitioned directories.
+#
+# `nomad.io.from_file` handles these cases with a single function call.
+
+# %%
+import glob
+import pandas as pd
+import nomad.io.base as loader
+import nomad.data as data_folder
+from pathlib import Path
+
+data_dir = Path(data_folder.__file__).parent
+
+# %% [markdown]
+# ## Pandas vs nomad.io for partitioned data
+#
+# Partitioned directories (e.g., `date=2024-01-01/`, `date=2024-01-02/`, ...) require a loop with pandas:
+
+# %%
+csv_files = glob.glob(str(data_dir / "partitioned_csv" / "*" / "*.csv"))
+df_list = []
+for f in csv_files:
+ df_list.append(pd.read_csv(f))
+df_pandas = pd.concat(df_list, ignore_index=True)
+
+print(f"Pandas: {len(df_pandas)} rows")
+print(df_pandas.dtypes)
+print("\nFirst few rows:")
+print(df_pandas.head(3))
+
+# %% [markdown]
+# `nomad.io.from_file` handles partitioned directories in one line, plus automatic type casting and column mapping:
+
+# %%
+traj_cols = {"user_id": "user_id",
+ "latitude": "dev_lat",
+ "longitude": "dev_lon",
+ "datetime": "local_datetime"}
+
+df = loader.from_file(data_dir / "partitioned_csv", format="csv", traj_cols=traj_cols, parse_dates=True)
+print(f"nomad.io: {len(df)} rows")
+print(df.dtypes)
+print("\nFirst few rows:")
+print(df.head(3))
+print("\nNote: 'local_datetime' is now datetime64[ns], not object!")
+
+# %% [markdown]
+# The same pattern works for Parquet files, with the type casting and processing relying on passing to the functions which columns correspond to the default "typical" spatio-temporal column names
+
+# %%
+traj_cols = {"user_id": "uid", "timestamp": "timestamp",
+ "latitude": "latitude", "longitude": "longitude", "date": "date"}
+
+df = loader.from_file(data_dir / "partitioned_parquet", format="parquet", traj_cols=traj_cols, parse_dates=True)
+print(f"Loaded {len(df)} rows")
+print(df.dtypes)
+
+# %%
+# These are the default canonical columnn names
+from nomad.constants import DEFAULT_SCHEMA
+print(DEFAULT_SCHEMA.keys())
diff --git a/examples/lachesis_demo.ipynb b/examples/lachesis_demo.ipynb
index a46d838b..cb9a8992 100644
--- a/examples/lachesis_demo.ipynb
+++ b/examples/lachesis_demo.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
- "id": "92838936",
+ "id": "05c3afed",
"metadata": {},
"source": [
"# Lachesis Stop Detection"
@@ -10,7 +10,7 @@
},
{
"cell_type": "markdown",
- "id": "cb276fd9",
+ "id": "bca8605a",
"metadata": {},
"source": [
"The first stop detection algorithm implemented in ```nomad``` is a sequential algorithm insipired by the one in _Project Lachesis: Parsing and Modeling Location Histories_ (Hariharan & Toyama). This algorithm for extracting stays is dependent on two parameters: the roaming distance and the stay duration. \n",
@@ -28,32 +28,43 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "19184dee",
- "metadata": {},
+ "execution_count": 1,
+ "id": "7f0b2bb1",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:46.921799Z",
+ "iopub.status.busy": "2025-11-24T18:32:46.921799Z",
+ "iopub.status.idle": "2025-11-24T18:32:51.000857Z",
+ "shell.execute_reply": "2025-11-24T18:32:51.000857Z"
+ }
+ },
"outputs": [],
"source": [
"%matplotlib inline\n",
+ "import matplotlib\n",
+ "matplotlib.use('TkAgg')\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.ion()\n",
"\n",
"# Imports\n",
"import nomad.io.base as loader\n",
+ "import geopandas as gpd\n",
"from shapely.geometry import box\n",
- "import matplotlib.pyplot as plt\n",
- "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode\n",
+ "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_stops, plot_pings\n",
"import nomad.stop_detection.lachesis as LACHESIS\n",
+ "import nomad.data as data_folder\n",
+ "from pathlib import Path\n",
"\n",
"# Load data\n",
+ "data_dir = Path(data_folder.__file__).parent\n",
+ "city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')\n",
+ "outer_box = box(*city.total_bounds)\n",
+ "\n",
"filepath_root = 'gc_data_long/'\n",
- "tc = {\n",
- " \"user_id\": \"gc_identifier\",\n",
- " \"timestamp\": \"unix_ts\",\n",
- " \"x\": \"dev_x\",\n",
- " \"y\": \"dev_y\",\n",
- " \"ha\":\"ha\",\n",
- " \"date\":\"date\"}\n",
+ "tc = {\"user_id\": \"gc_identifier\", \"x\": \"dev_x\", \"y\": \"dev_y\", \"timestamp\": \"unix_ts\"}\n",
"\n",
"users = ['admiring_brattain']\n",
- "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','==', '2024-01-01'), traj_cols=tc)\n",
+ "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)\n",
"\n",
"# Lachesis (sequential stop detection)\n",
"stops = LACHESIS.lachesis(traj, delta_roam=20, dt_max = 60, dur_min=5, complete_output=True, keep_col_names=True, traj_cols=tc)"
@@ -61,28 +72,32 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "570b6103",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAACYCAYAAAD5s4rEAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAYdklEQVR4nO3de1AV5x3G8WchcolcDIgXIiDeQqVWq8ZrbctEjaYk07FqNCMmaFNjqk6qTsWa1Puk1aR1pg46maDUmhrMFBs0qTMab2k1qRZJUm2iSURgFFFTBS8IwvYPh9OccDnnwHm5ne9nhlHefffd9+zub5fHPRwt27ZtAQAAAAAAr/Nr6QkAAAAAANBeEboBAAAAADCE0A0AAAAAgCGEbgAAAAAADCF0AwAAAABgCKEbAAAAAABDCN0AAAAAABhC6AYAAAAAwBBCNwAAAAAAhhC6AQBtXmZmpizL0okTJ5ptmz179lRycnKzbU+SDh06JMuydOjQoWbZXnp6ujIzM5tlWwAAtFeEbgAA2ojBgwfr2LFjGjx4cLNsj9ANAEDT3dfSEwAAAO4JCwvTiBEjWnoaAADAAzzpBgD4hPLyci1atEiDBg1SeHi4IiIiNHLkSL399tu1+lZXV+sPf/iDBg0apODgYHXq1EkjRoxQTk5Orb579+7V4MGDFRwcrISEBG3ZsqVWn+LiYs2ZM0c9evRQQECA4uPjtXLlSt29e9ep36ZNmzRw4ECFhIQoNDRUCQkJ+tWvfuVYXtfby7/88ktNmzZN0dHRCgwMVNeuXfXII48oLy+vwf3har2ePXvq1KlTOnz4sCzLkmVZ6tmzp2P9goICzZgxQ126dFFgYKC+9a1v6dVXX1V1dbWjT35+vizL0rp167R27VrFxsYqKChIQ4cO1Xvvvec0n8uXL+tnP/uZYmJiFBgYqKioKI0ePVr79+9v8HUAANDa8aQbAOAT7ty5o6+++kqLFy/Wgw8+qIqKCu3fv1+TJk3S1q1bNXPmTEffZ555Rtu3b9fs2bO1atUqBQQEKDc3V/n5+U5jfvTRR1q0aJHS0tLUtWtXvf7665o9e7b69Omj73//+5LuBe5hw4bJz89Pv/71r9W7d28dO3ZMa9asUX5+vrZu3SpJevPNN/X8889r/vz5euWVV+Tn56fPP/9cp0+fbvB1PfbYY6qqqtK6desUGxurK1eu6OjRo7p27VqT1tu1a5cmT56s8PBwpaenS5ICAwMl3QvIo0aNUkVFhVavXq2ePXtqz549Wrx4sb744gtH/xobN25UXFycNmzYoOrqaq1bt04TJ07U4cOHNXLkSElSSkqKcnNztXbtWvXr10/Xrl1Tbm6url692uDrAACg1bMBAGjjtm7dakuyjx8/7vY6d+/etSsrK+3Zs2fb3/3udx3tR44csSXZy5Yta3D9uLg4OygoyD5//ryj7fbt23ZERIQ9Z84cR9ucOXPskJAQp362bduvvPKKLck+deqUbdu2PW/ePLtTp04NbvPgwYO2JPvgwYO2bdv2lStXbEn2hg0b3HrNNdxdLzEx0f7BD35Qqz0tLc2WZH/44YdO7XPnzrUty7I/++wz27Zt+9y5c7YkOzo62r59+7ajX2lpqR0REWGPHTvW0RYSEmK/8MILHr0OAADaAt5eDgDwGW+99ZZGjx6tkJAQ3XffferQoYMyMjL0n//8x9Hnb3/7myTp5z//ucvxBg0apNjYWMf3QUFB6tevn86fP+9o27Nnj5KSkhQdHa27d+86viZOnChJOnz4sCRp2LBhunbtmqZPn663335bV65ccbn9iIgI9e7dW+vXr9fvfvc7nTx50unt3d5er8aBAwfUv39/DRs2zKn9mWeekW3bOnDggFP7pEmTFBQU5Pg+NDRUjz/+uI4cOaKqqipJ915/Zmam1qxZow8++ECVlZVuzwcAgNaM0A0A8AnZ2dmaOnWqHnzwQW3fvl3Hjh3T8ePHNWvWLJWXlzv6Xb58Wf7+/urWrZvLMSMjI2u1BQYG6vbt247vL126pN27d6tDhw5OX4mJiZLkCNcpKSnasmWLzp8/r5/85Cfq0qWLhg8frn379tW7fcuy9N577+nRRx/VunXrNHjwYEVFRWnBggUqKyvz+no1rl69qu7du9dqj46Odiz/urr2Zbdu3VRRUaEbN25IkrKysvT000/r9ddf18iRIxUREaGZM2equLjY5XwAAGjN+J1uAIBP2L59u+Lj45WVlSXLshztd+7cceoXFRWlqqoqFRcX1xksPdW5c2d95zvf0dq1a+tcXhNUJSk1NVWpqam6efOmjhw5ouXLlys5OVlnzpxRXFxcnevHxcUpIyNDknTmzBnt3LlTK1asUEVFhTZv3lzvvBq7nnTvHxsuXrxYq/3ChQuO1/x1dQXn4uJiBQQEKCQkxLHOhg0btGHDBhUUFCgnJ0dpaWkqKSnR3r17G5wPAACtGU+6AQA+wbIsBQQEOAXu4uLiWp9eXvO2702bNnllu8nJyfr3v/+t3r17a+jQobW+vh66a3Ts2FETJ07UsmXLVFFRoVOnTrm1rX79+unFF1/UgAEDlJub6/Yc61vvm0/tazzyyCM6ffp0rW1s27ZNlmUpKSnJqT07O9vp3QRlZWXavXu3xowZI39//1rjx8bGat68eRo3bpxHrwMAgNaIJ90AgHbjwIEDtT5hXLr3Sd3JycnKzs7W888/r8mTJ6uwsFCrV69W9+7ddfbsWUffMWPGKCUlRWvWrNGlS5eUnJyswMBAnTx5Uvfff7/mz5/v0ZxWrVqlffv2adSoUVqwYIEeeughlZeXKz8/X++++642b96sHj166Nlnn1VwcLBGjx6t7t27q7i4WC+//LLCw8P18MMP1zn2xx9/rHnz5mnKlCnq27evAgICdODAAX388cdKS0urd07urjdgwAC9+eabysrKUq9evRQUFKQBAwboF7/4hbZt26Yf/ehHWrVqleLi4vTOO+8oPT1dc+fOVb9+/Zy25+/vr3HjxmnhwoWqrq7Wb3/7W5WWlmrlypWSpOvXryspKUlPPfWUEhISFBoaquPHj2vv3r2aNGmSR/sbAIDWhtANAGg3lixZUmf7uXPnlJqaqpKSEm3evFlbtmxRr169lJaWpqKiIkf4q5GZmanBgwcrIyNDmZmZCg4OVv/+/Z3+z2x3de/eXSdOnNDq1au1fv16FRUVKTQ0VPHx8ZowYYIeeOABSffCfmZmpnbu3Kn//ve/6ty5s773ve9p27ZtioqKqnPsbt26qXfv3kpPT1dhYaEsy1KvXr306quvNviPA+6ut3LlSl28eFHPPvusysrKFBcXp/z8fEVFReno0aNaunSpli5dqtLSUvXq1Uvr1q3TwoULa21v3rx5Ki8v14IFC1RSUqLExES98847Gj16tKR7H0A3fPhw/elPf1J+fr4qKysVGxurJUuW6Je//KXH+xwAgNbEsm3bbulJAACA9ic/P1/x8fFav369Fi9e3NLTAQCgRfA73QAAAAAAGELoBgAAAADAEN5eDgAAAACAITzpBgAAAADAEEI3AAAAAACGELoBAAAAADCE0A0AAAAAgCGEbgAAAAAADCF0AwAAAABgCKEbAAAAAABDCN0AAAAAABhC6AYAAAAAwBBCNwAAAAAAhhC6AQAAAAAwhNANAAAAAIAhhG4AAAAAAAwhdAMAAAAAYAihGwAAAAAAQwjdAAAAAAAYQugGAAAAAMAQQjcAAAAAAIYQugEAAAAAMITQDQAAAACAIYRuAAAAAAAMIXQDAAAAAGAIoRsAAAAAAEMI3QAAAAAAGELoBgAAAADAkPvc6VRdXa0LFy4oNDRUlmWZnhMAAAAAAK2abdsqKytTdHS0/Pzqf57tVui+cOGCYmJivDY5AAAAAADag8LCQvXo0aPe5W6F7tDQUMdgYWFh3pkZWqUvvpCefFIKCJCCglp6Ni2nvFyqqJCysqTevVt6NoD3tMYa96TevDF/b9e3yX3qq9ei1nieNjdfPfatTUudi754/Ftz3fvi8YB7SktLFRMT48jL9XErdNe8pTwsLIzQ3c6FhEj+/lLHjtL997f0bFqOv79UVXVvf3DKoz1pjTXuSb15Y/7erm+T+9RXr0Wt8Txtbr567FubljoXffH4t+a698XjAc+4+hVsPkgNAAAAAABDCN0AAAAAABhC6AYAAAAAwBBCNwAAAAAAhhC6AQAAAAAwhNANAAAAAIAhhG4AAAAAAAwhdAMAAAAAYAihGwAAAAAAQwjdAAAAAAAYQuhuoh07dri9rKG+7o7tbltTlJZ6dzxTLl0yO8+m7IcdO3Z4dG64aveGmrG/+ac75s+f3yyv5+v96/t7Y8bypL+7+8fd+vZ0HE+XN0Zp6Q5dvereuK7qrDF1WNc6X683V/uyrtq8dGmHR3Np6nXum3M0dd28enWHY+yGzk1P70WNPa++WZcNzaWuGvbkvlhcPN/lfLx9H6hvPNP3m/q248555Y1j6ardm+eQNzT15ylP12/oOLh7bjSmnzevK005Xs15rN2pe095q35N/nzsjePjjePk6mevlqz7to7Q3USE7pZTUkLo9kRTQvdbb71F6HYxfnsN3a7qrDF1WNc6TQ3dJSU7PJoLobv1h+6ysrdczsfb94H6xjN9v6lvO4TuurWm0O3uudGYfr4Yut2pe095q34J3YTupiB0AwAAAABgCKEbAAAAAABDCN0AAAAAABhC6AYAAAAAwBBCNwAAAAAAhhC6AQAAAAAwhNANAAAAAIAhhG4AAAAAAAwhdAMAAAAAYAihGwAAAAAAQwjdAAAAAAAYQugGAAAAAMAQQjcAAAAAAIYQugEAAAAAMITQDQAAAACAIYRuAAAAAAAMIXQDAAAAAGAIoRsAAAAAAEMI3QAAAAAAGELoBgAAAADAEEI3AAAAAACGELoBAAAAADCE0A0AAAAAgCGEbgAAAAAADCF0AwAAAABgCKEbAAAAAABDCN0AAAAAABhC6AYAAAAAwBBCNwAAAAAAhhC6AQAAAAAwhNANAAAAAIAhhG4AAAAAAAwhdAMAAAAAYAihGwAAAAAAQwjdAAAAAAAYQugGAAAAAMAQQjcAAAAAAIYQugEAAAAAMOS+lp5AWzd9+nS3lzXU192x3W1rirAw745nSpcuZufZlP3g6pjUt9zbx7Kusb/5pzumTJmiUaNGuRzb3XZ3xqnv740Zy5P+7u4fd+u7seeCu8sbIyxsuoKD3evrqs4aU4d1rfP1enO1L+uqTU/n0dTr3DfnaOq6GRk5XbdvO2/T0/tAU+9F9a3r6jjV1deTuYSGTnE5H2/fB+obz/T9pr7tuHNeNfZ4enLN8uY55A1N/XnK0/UbOg7unhuN6efN64q36t40d+reU96qX5M/H3vj+HjjOLm6xrd07bdllm3btqtOpaWlCg8P1/Xr1xUWFtYc80ILOXtWeuIJKSxMuv/+lp5Ny7l1SyotlXJypL59W3o2gPe0xhr3pN68MX9v17fJfeqr16LWeJ42N1899q1NS52Lvnj8W3Pd++LxgHvczcm8vRwAAAAAAEMI3QAAAAAAGELoBgAAAADAEEI3AAAAAACGELoBAAAAADCE0A0AAAAAgCGEbgAAAAAADCF0AwAAAABgCKEbAAAAAABDCN0AAAAAABhynzudbNuWJJWWlhqdDFrejRtSVZV08+a9P31Vefm913/jhsRpj/akNda4J/Xmjfl7u75N7lNfvRa1xvO0ufnqsW9tWupc9MXj35rr3hePB9xTk49r8nJ9LNtVD0lFRUWKiYnxzswAAAAAAGgnCgsL1aNHj3qXuxW6q6urdeHCBYWGhsqyLK9OsC0pLS1VTEyMCgsLFRYW1tLTAdok6gjwDmoJ8A5qCfAOX6wl27ZVVlam6Oho+fnV/5vbbr293M/Pr8Hk7mvCwsJ85kQCTKGOAO+glgDvoJYA7/C1WgoPD3fZhw9SAwAAAADAEEI3AAAAAACGELo9EBgYqOXLlyswMLClpwK0WdQR4B3UEuAd1BLgHdRS/dz6IDUAAAAAAOA5nnQDAAAAAGAIoRsAAAAAAEMI3QAAAAAAGELoBgAAAADAkHYduo8cOaLHH39c0dHRsixLf/3rX2v1yc7O1qOPPqrOnTvLsizl5eW5PX5RUZECAgKUkJBQ53LLsmRZlj744AOn9jt37igyMlKWZenQoUMevCKgZbiqpcrKSi1ZskQDBgxQx44dFR0drZkzZ+rChQsNjrtixQpHnfj7+ysmJkY//elPdfnyZUcf6gjtTXp6uuLj4xUUFKQhQ4bo/fffd1remPsStQRf1FAtcV8C3OfqvrRixQolJCSoY8eOeuCBBzR27Fh9+OGHDY5JLTlr16H75s2bGjhwoDZu3Nhgn9GjR+s3v/mNx+NnZmZq6tSpunXrlv7xj3/U2ScmJkZbt251atu1a5dCQkI83h7QUlzV0q1bt5Sbm6uXXnpJubm5ys7O1pkzZ/TEE0+4HDsxMVEXL15UQUGBNm3apN27d2vmzJlOfagjtBdZWVl64YUXtGzZMp08eVJjxozRxIkTVVBQ4OjT2PsStQRf4qqWuC8B7nHnvtSvXz9t3LhRn3zyif7+97+rZ8+eGj9+vFOArgu19DW2j5Bk79q1q97l586dsyXZJ0+edGu86upqu1evXvbevXvtJUuW2KmpqXVu88UXX7TDwsLsW7duOdrHjRtnv/TSS7Yk++DBgx6+EqBluaqlGv/85z9tSfb58+fr7bN8+XJ74MCBTm1r1qyx/fz8HDVDHaE9GTZsmP3cc885tSUkJNhpaWm1+npyX6KW4Gs8qaUa3JeA2hpTS9evX7cl2fv376+3D7XkrF0/6Tbp4MGDunXrlsaOHauUlBTt3LlTZWVltfoNGTJE8fHx+stf/iJJKiws1JEjR5SSktLcUwaa1fXr12VZljp16uTResHBwaqurtbdu3cdbdQR2oOKigr961//0vjx453ax48fr6NHj3p9e9QS2qvG1hL3JcBZY2qpoqJCr732msLDwzVw4ECPtufLtUTobqSMjAxNmzZN/v7+SkxMVJ8+fZSVlVVn39TUVG3ZskWStHXrVj322GOKiopqzukCzaq8vFxpaWl66qmnFBYW5vZ6n376qTZt2qRhw4YpNDTUaRl1hLbuypUrqqqqUteuXZ3au3btquLiYq9ui1pCe9aYWuK+BNTmSS3t2bNHISEhCgoK0u9//3vt27dPnTt3dntbvl5LhO5GuHbtmrKzszVjxgxH24wZMxwnyzfNmDFDx44d05dffqnMzEzNmjWruaYKNLvKykpNmzZN1dXVSk9Pd9n/k08+UUhIiIKDg9W/f3/FxMTojTfeqNWPOkJ7YVmW0/e2bddqawxqCb7G3VrivgQ0zJ1aSkpKUl5eno4ePaoJEyZo6tSpKikpaXBcaun/7mvpCbRFf/7zn1VeXq7hw4c72mzbVnV1tU6fPq3+/fs79Y+MjFRycrJmz56t8vJyTZw4sc63ogNtXWVlpaZOnapz587pwIEDbj1NeOihh5STkyN/f39FR0crMDCwzn7UEdq6zp07y9/fv9bTg5KSklpPGRqDWoKv8KSWuC8B9fOkljp27Kg+ffqoT58+GjFihPr27auMjAwtXbq03vGppf/jSXcjZGRkaNGiRcrLy3N8ffTRR0pKSqr3afesWbN06NAhzZw5U/7+/s08Y8C8mh9szp49q/379ysyMtKt9QICAtSnTx/Fx8fXezGuQR2hLQsICNCQIUO0b98+p/Z9+/Zp1KhRXhmfWoIvcLeWuC8BDWvKfcm2bd25c8fl+NTSPe36SfeNGzf0+eefO74/d+6c8vLyFBERodjYWEnSV199pYKCAsf/2/jZZ59Jkrp166Zu3brVGjMvL0+5ubl64403av3/3NOnT9eyZcv08ssvq0OHDk7LJkyYoMuXL3v0e0RAa+Gqlu7evavJkycrNzdXe/bsUVVVleNfTSMiIhQQEOCVeVBHaOsWLlyolJQUDR06VCNHjtRrr72mgoICPffcc44+nt6XGoNaQlvnqpa4LwHucVVLN2/e1Nq1a/XEE0+oe/fuunr1qtLT01VUVKQpU6Z4bR7tvZbadeg+ceKEkpKSHN8vXLhQkvT0008rMzNTkpSTk6PU1FRHn2nTpkmSli9frhUrVtQaMyMjQ/37968VuCXpxz/+sebOnavdu3dr0qRJTsssy/LowwaA1sRVLRUVFSknJ0eSNGjQIKd1Dx48qB/+8IdemQd1hLbuySef1NWrV7Vq1SpdvHhR3/72t/Xuu+8qLi7O0cfT+1JjUEto61zVEvclwD2uasnf31+ffvqp/vjHP+rKlSuKjIzUww8/rPfff1+JiYlem0d7ryXLtm27pScBAAAAAEB7xO90AwAAAABgCKEbAAAAAABDCN0AAAAAABhC6AYAAAAAwBBCNwAAAAAAhhC6AQAAAAAwhNANAAAAAIAhhG4AAAAAAAwhdAMAAAAAYAihGwAAAAAAQwjdAAAAAAAYQugGAAAAAMCQ/wEqxlazKR18KgAAAABJRU5ErkJggg==",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "execution_count": 2,
+ "id": "86273598",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:32:51.005525Z",
+ "iopub.status.busy": "2025-11-24T18:32:51.000857Z",
+ "iopub.status.idle": "2025-11-24T18:32:51.355886Z",
+ "shell.execute_reply": "2025-11-24T18:32:51.355274Z"
}
- ],
+ },
+ "outputs": [],
"source": [
- "fig, ax_barcode = plt.subplots(figsize=(10,1.5))\n",
+ "fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),\n",
+ " gridspec_kw={'height_ratios':[10,1]})\n",
+ "\n",
+ "gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')\n",
+ "city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')\n",
+ "\n",
+ "plot_stops(stops, ax=ax_map, cmap='Blues')\n",
+ "plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)\n",
+ "ax_map.set_axis_off()\n",
+ "\n",
+ "plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)\n",
+ "plot_stops_barcode(stops, ax=ax_barcode, cmap='Blues', set_xlim=False, timestamp='unix_ts')\n",
"\n",
- "plot_time_barcode(traj[tc['timestamp']], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops, ax=ax_barcode, stop_color='blue', set_xlim=False, timestamp='unix_ts')\n",
- "fig.suptitle(\"Lachesis stops\")\n",
- "plt.tight_layout()\n",
+ "plt.tight_layout(pad=0.1)\n",
"plt.show()"
]
}
@@ -103,7 +118,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.5"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/examples/lachesis_demo.py b/examples/lachesis_demo.py
index 6598fb9e..fc916225 100644
--- a/examples/lachesis_demo.py
+++ b/examples/lachesis_demo.py
@@ -30,29 +30,47 @@
# %%
# %matplotlib inline
+import matplotlib
+matplotlib.use('TkAgg')
+import matplotlib.pyplot as plt
+plt.ion()
# Imports
import nomad.io.base as loader
+import geopandas as gpd
from shapely.geometry import box
-import matplotlib.pyplot as plt
-from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode
+from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_stops, plot_pings
import nomad.stop_detection.lachesis as LACHESIS
+import nomad.data as data_folder
+from pathlib import Path
# Load data
+data_dir = Path(data_folder.__file__).parent
+city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')
+outer_box = box(*city.total_bounds)
+
filepath_root = 'gc_data_long/'
tc = {"user_id": "gc_identifier", "x": "dev_x", "y": "dev_y", "timestamp": "unix_ts"}
users = ['admiring_brattain']
-traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','==', '2024-01-01'), traj_cols=tc)
+traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)
# Lachesis (sequential stop detection)
stops = LACHESIS.lachesis(traj, delta_roam=20, dt_max = 60, dur_min=5, complete_output=True, keep_col_names=True, traj_cols=tc)
# %%
-fig, ax_barcode = plt.subplots(figsize=(10,1.5))
+fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),
+ gridspec_kw={'height_ratios':[10,1]})
+
+gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')
+city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')
+
+plot_stops(stops, ax=ax_map, cmap='Blues')
+plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)
+ax_map.set_axis_off()
plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)
-plot_stops_barcode(stops, ax=ax_barcode, stop_color='blue', set_xlim=False, timestamp='unix_ts')
-fig.suptitle("Lachesis stops")
-plt.tight_layout()
+plot_stops_barcode(stops, ax=ax_barcode, cmap='Blues', set_xlim=False, timestamp='unix_ts')
+
+plt.tight_layout(pad=0.1)
plt.show()
diff --git a/examples/tadbscan_demo.ipynb b/examples/tadbscan_demo.ipynb
index 6e28d6b4..c4f5747b 100644
--- a/examples/tadbscan_demo.ipynb
+++ b/examples/tadbscan_demo.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
- "id": "92838936",
+ "id": "7c76d1d8",
"metadata": {},
"source": [
"# TADBSCAN Stop Detection"
@@ -10,7 +10,7 @@
},
{
"cell_type": "markdown",
- "id": "cb276fd9",
+ "id": "f29a96ce",
"metadata": {},
"source": [
"The second stop detection algorithm implemented in ```nomad``` is an adaptation of DBSCAN. Unlike in plain DBSCAN, we also incorporate the time dimension to determine if two pings are \"neighbors\". This implementation relies on 3 parameters\n",
@@ -24,79 +24,81 @@
},
{
"cell_type": "code",
- "execution_count": 4,
- "id": "19184dee",
- "metadata": {},
+ "execution_count": 1,
+ "id": "1e62c25a",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:03.240035Z",
+ "iopub.status.busy": "2025-11-24T18:33:03.240035Z",
+ "iopub.status.idle": "2025-11-24T18:33:05.816985Z",
+ "shell.execute_reply": "2025-11-24T18:33:05.816985Z"
+ }
+ },
"outputs": [],
"source": [
"%matplotlib inline\n",
+ "import matplotlib\n",
+ "matplotlib.use('TkAgg')\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.ion()\n",
"\n",
"# Imports\n",
"import nomad.io.base as loader\n",
"import geopandas as gpd\n",
"from shapely.geometry import box\n",
- "import pandas as pd\n",
- "import numpy as np\n",
- "import matplotlib.pyplot as plt\n",
- "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode\n",
+ "from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_stops, plot_pings\n",
"import nomad.stop_detection.dbscan as DBSCAN\n",
- "import nomad.filters as filters \n",
- "import nomad.stop_detection.postprocessing as post\n",
"\n",
"# Load data\n",
- "city = gpd.read_file(\"garden_city.geojson\").to_crs('EPSG:3857')\n",
- "outer_box = box(*city.total_bounds).buffer(15, join_style='mitre')\n",
+ "import nomad.data as data_folder\n",
+ "from pathlib import Path\n",
+ "data_dir = Path(data_folder.__file__).parent\n",
+ "city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')\n",
+ "outer_box = box(*city.total_bounds)\n",
"\n",
"filepath_root = 'gc_data_long/'\n",
- "tc = {\n",
- " \"user_id\": \"gc_identifier\",\n",
- " \"timestamp\": \"unix_ts\",\n",
- " \"x\": \"dev_x\",\n",
- " \"y\": \"dev_y\",\n",
- " \"ha\":\"ha\",\n",
- " \"date\":\"date\"}\n",
+ "tc = {\"user_id\": \"gc_identifier\", \"x\": \"dev_x\", \"y\": \"dev_y\", \"timestamp\": \"unix_ts\"}\n",
"\n",
"# Density based stop detection (Temporal DBSCAN)\n",
- "users = ['confident_aryabhata']\n",
- "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','<=', '2024-01-03'), traj_cols=tc)\n",
- "traj[['longitude','latitude']] = np.column_stack(\n",
- " filters.to_projection(traj, x='dev_x', y='dev_y', data_crs='EPSG:3857', crs_to='EPSG:4326')\n",
- ")\n",
+ "users = ['admiring_brattain']\n",
+ "traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)\n",
"\n",
- "user_data_tadb = traj.assign(cluster=DBSCAN.ta_dbscan_labels(traj, time_thresh=240, dist_thresh=15, min_pts=3, traj_cols=tc))\n",
"stops_tadb = DBSCAN.ta_dbscan(traj,\n",
" time_thresh=720,\n",
" dist_thresh=15,\n",
" min_pts=3,\n",
" complete_output=True,\n",
- " traj_cols=tc)\n",
- "stops_tadb[\"cluster\"] = post.remove_overlaps(user_data_tadb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3) "
+ " traj_cols=tc) "
]
},
{
"cell_type": "code",
- "execution_count": 5,
- "id": "2159107b",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAA9oAAACMCAYAAACDKNEwAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAp9ElEQVR4nO3deXxU1d3H8e9kyGQhCxACJRC2qGwxWFbBUnaRTQXUAsoiqPUpYkFUtC4Ea4uIIlgU9DEsigZEAQUECgjiEhAr2AcBl0oEhCgGSSIQkOQ8f9C5zJpMwmQI8nm/XvOCOXPuOeeee+Y385u5c2MzxhgBAAAAAICgCDvfAwAAAAAA4NeERBsAAAAAgCAi0QYAAAAAIIhItAEAAAAACCISbQAAAAAAgohEGwAAAACAICLRBgAAAAAgiEi0AQAAAAAIIhJtAAAAAACCiEQbAMrAZrMFdNu0aZO1zbPPPiubzabU1NQy9bVp0ya3Nh0OhxITE3XVVVfpoYce0rfffuu1zfz5873GkpiYqC5dumjlypVe9Xfv3q1hw4apcePGioyMVM2aNdWqVSvdddddys/P96q/YsUK9e/fX7Vr15bD4VCNGjXUvXt3vfrqq/rll1+86v/444+KiIiQzWbTJ5984nM/R44cKZvNphYtWqioqMjrcZvNprvuuiuQKSvRwYMHlZ6erh07dpxzW5VNdna2bDab5s+fb5V99NFHSk9P19GjR73qN2zYUP369QvdAIPgnXfeUXp6+vkexgVl5MiRatiw4fkeBgBclEi0AaAMsrKy3G59+vRRVFSUV3mrVq2sbebOnStJ+vzzz7V169Yy9/n3v/9dWVlZ2rhxozIyMtSlSxfNnTtXzZo106uvvupzm3nz5ikrK0sfffSRXnzxRdntdvXv318rVqyw6mzfvl2tW7fWrl279Oijj2rNmjWaM2eO+vbtq7Vr1+rIkSNWXWOMbr31Vl177bUqLi7W9OnTtX79ei1YsEAtW7bUn/70Jz3//PNe43jllVd06tQpSVJGRkaJ+7lr1y63RDHYDh48qMmTJ/8qE+06deooKytLffv2tco++ugjTZ482WeifSF65513NHny5PM9jAvKI488omXLlp3vYQDARanK+R4AAFxIrrzySrf7iYmJCgsL8yp3+uSTT/TZZ5+pb9++WrVqlTIyMtS+ffsy9XnppZe6tX/ttddqwoQJ6tGjh0aOHKm0tDRdfvnlbtukpqaqTZs21v1rrrlG1atXV2Zmpvr37y9JmjFjhsLCwrRp0ybFxsZadW+44Qb99a9/lTHGKps2bZrmz5+vyZMn69FHH3Xrq3///rr//vv19ddfe4197ty5qlWrlho0aKDMzExNnz5dUVFRXvWqVq2qVq1aadKkSRo6dKjPOvAvIiLC7xpE+fzyyy+y2WyqUuXCfauUkpJyvocAABctvtEGgArk/Bb3iSeeUMeOHbVo0SIdP378nNutUaOGXnjhBZ0+fVrPPPNMqfUjIyPlcDgUHh5uleXm5iouLk4xMTE+t7HZbJLOJBxTp05V06ZN9cgjj/is+5vf/Ea/+93v3Mq2bt2qnTt3atiwYbr99tuVl5enN9980+8Yp06dqu+++04zZ84sdX98WbJkidq3b6/4+HhFR0ercePGGjVqlKQzp+G3bdtWknTrrbdap9W7nor89ttvq0OHDoqOjlZsbKx69uyprKwstz7S09Nls9m0fft2DRw4UHFxcYqPj9ctt9yiw4cPu9V999131aVLFyUkJCgqKkr169fXoEGDSjz+9913n+Lj491OoR87dqxsNpumTZtmleXm5iosLEz/+Mc/JHmfOp6enq777rtPktSoUSOfP2mQpDVr1qhVq1aKiopS06ZNrbMvSuLs68knn9Tf/vY31a9fX5GRkWrTpo02bNjgVf+DDz5Q9+7dFRsbq+joaHXs2FGrVq1yq3P8+HHde++9atSokSIjI1WjRg21adNGmZmZks6cAv3cc89Jcv/5RnZ2doljdZ4iv2zZMqWlpSkyMlKNGzfWs88+61bP+TONV155RRMmTFDdunUVERFhfXg0d+5ctWzZ0hrbgAEDtHv3bq/+tm7dqv79+yshIUGRkZFKSUnRuHHj3Op89dVXGjp0qGrVqqWIiAg1a9bM2jen4uJiPf7442rSpImioqJUrVo1paWluT03Dh8+rDvuuEPJycmKiIiwflayfv16q46vU8edP8V45ZVX1KxZM0VHR6tly5Y+f1ry1ltvKS0tTREREWrcuLFmzpxpPQcAACUj0QaACnLixAllZmaqbdu2Sk1N1ahRo1RQUKAlS5YEpf22bduqTp062rx5s9djRUVFOn36tH755RcdOHBA48aN07FjxzR06FCrTocOHXTo0CHdfPPNeu+993TixAmf/XzyySc6cuSIrrvuujK9wXZ+yDBq1CgNHjxY0dHRJZ4+3qFDBw0YMEBTp051O209EFlZWfrDH/6gxo0ba9GiRVq1apUeffRRnT59WpLUqlUrzZs3T5L08MMPW6f433bbbZKk1157Tdddd53i4uKUmZmpjIwM/fTTT+rSpYs++OADr/4GDBigSy65RG+88YbS09O1fPly9erVy/qdenZ2tvr27SuHw6G5c+dqzZo1euKJJ1S1alXrVHpfevToofz8fH388cdW2fr16xUVFaV169ZZZRs2bJAxRj169PDZzm233aaxY8dKkpYuXerzJw2fffaZJkyYoPHjx1sJ1ejRo32uJ19mzZqlNWvWaMaMGVq4cKHCwsLUu3dvtw8n3nvvPXXr1k15eXnKyMhQZmamYmNj1b9/fy1evNiqd88992j27Nm6++67tWbNGr3yyiu68cYblZubK+nMKdA33HCDJPefb9SpU6fUce7YsUPjxo3T+PHjtWzZMnXs2FF//vOf9dRTT3nVffDBB7Vv3z7NmTNHK1asUK1atTRlyhSNHj1aLVq00NKlSzVz5kz9+9//VocOHfTVV19Z265du1adOnXSvn37NH36dK1evVoPP/ywvv/+e6vOrl271LZtW+3cuVNPP/20Vq5cqb59++ruu+92Oy3+ySefVHp6uoYMGaJVq1Zp8eLFGj16tNvPAIYNG6bly5fr0Ucf1T//+U+99NJL6tGjhzVnJVm1apVmzZqlxx57TG+++ab14cE333xj1VmzZo0GDhyohIQELV68WE8++aQyMzO1YMGCUtsHAEgyAIByGzFihKlatarPx15++WUjycyZM8cYY0xBQYGJiYkxnTp1CqjtjRs3GklmyZIlfuu0b9/eREVFWffnzZtnJHndIiIizPPPP++2bWFhobn++uutOna73fz2t781Dz30kPnhhx+seosWLXLbj0AcO3bMxMXFmSuvvNIqGzFihLHZbObrr792q+s6h3v27DF2u91MmDDBelySGTNmTIn9PfXUU0aSOXr0qN8627ZtM5LMvHnz3MqLiopMUlKSufzyy01RUZFVXlBQYGrVqmU6duxolU2aNMlIMuPHj3dr49VXXzWSzMKFC40xxrzxxhtGktmxY0eJ4/Z07Ngx43A4zGOPPWaMMebAgQNGkpk4caKJiooyhYWFxhhjbr/9dpOUlGRtt3fvXq99mzZtmpFk9u7d69VPgwYNTGRkpPn222+tshMnTpgaNWqYP/7xjyWO0dlXUlKSOXHihFWen59vatSoYXr06GGVXXnllaZWrVqmoKDAKjt9+rRJTU019erVM8XFxcYYY1JTU831119fYr9jxowxZX3b0qBBA2Oz2byOQ8+ePU1cXJw5duyYMebsc+33v/+9W72ffvrJREVFmT59+riV79u3z0RERJihQ4daZSkpKSYlJcVtTjz16tXL1KtXz+Tl5bmV33XXXSYyMtIcOXLEGGNMv379zBVXXFHivsXExJhx48aVWGfEiBGmQYMGbmWSTO3atU1+fr5VlpOTY8LCwsyUKVOssrZt25rk5GRz8uRJq6ygoMAkJCSU+TgAwMWIb7QBoIJkZGQoKipKgwcPliTFxMToxhtv1Pvvv+/2Tdjp06fdbsblt9Gl8Vf35Zdf1rZt27Rt2zatXr1aI0aM0JgxYzRr1iyrTkREhJYtW6Zdu3bpmWee0eDBg3X48GH97W9/U7NmzfTFF1+Uc8+l119/Xfn5+dap29KZb7aNMdY3y740adJEo0eP1qxZs7Rv376A+3OeFn7TTTfp9ddf13fffRfwtl988YUOHjyoYcOGKSzs7MtiTEyMBg0apC1btnid7n3zzTe73b/ppptUpUoVbdy4UZJ0xRVXyOFw6I477tCCBQvcviksSXR0tDp06GCd/rtu3TpVq1ZN9913n06dOmV9u75+/Xq/32YH6oorrlD9+vWt+5GRkbrssst8Xs3el4EDByoyMtK67/ymevPmzSoqKtKxY8e0detW3XDDDW4/T7Db7Ro2bJgOHDhgrbF27dpp9erVeuCBB7Rp0ya/Z1f4Ulxc7Pb88bxyfYsWLdSyZUu3sqFDhyo/P1+ffvqpW/mgQYPc7mdlZenEiRMaOXKkW3lycrK6detmnSr/5Zdf6j//+Y9Gjx7tNieuCgsLtWHDBg0YMEDR0dFuY+7Tp48KCwu1ZcsWaz4+++wz/elPf9LatWt9/gWAdu3aaf78+Xr88ce1ZcsWn1f996dr165u12WoXbu2atWqZR37Y8eO6ZNPPtH1118vh8Nh1YuJibGu8QAAKBmJNgBUgK+//lqbN29W3759ZYzR0aNHdfToUev0V+dvYbOzsxUeHu52e++99wLuZ9++fUpKSvIqb9asmdq0aaM2bdrommuu0QsvvKCrr75a999/v9dVqJs1a6Zx48Zp4cKF1mmvubm51u+xncnY3r17Ax5XRkaGIiMjdc0111j7npaWpoYNG2r+/Pk+/4yXU3p6uux2u9/fg/vy+9//XsuXL9fp06c1fPhw1atXT6mpqdZvfEviPNXW12nISUlJKi4u1k8//eRW/pvf/MbtfpUqVZSQkGC1lZKSovXr16tWrVoaM2aMUlJSlJKSEtDvz3v06KEtW7bo2LFjWr9+vbp166aEhAS1bt1a69ev1969e7V3795zTrQTEhK8yiIiIgJOcj3nwFl26tQp/fzzz/rpp59kjPE7r9LZuX/22Wc1ceJELV++XF27dlWNGjV0/fXXu30g5c+oUaPcnj/du3cPaJyu/Tt5jrW0teF83Pn7/Hr16vkdZ25urk6fPq1//OMfXs/5Pn36SDrz5/CkM6ewP/XUU9qyZYt69+6thIQEde/e3e1P5C1evFgjRozQSy+9pA4dOqhGjRoaPny4cnJy/I7BqbRj7zx2tWvX9qrnqwwA4I1EGwAqwNy5c2WM0RtvvKHq1atbN+efX1qwYIGKioqUlJRkffPsvLVu3TqgPj7++GPl5OSoS5cuAdVPS0vTiRMn9OWXX/qtY7PZNH78eFWrVk07d+6UJLVp00Y1atTQW2+9FdC37V9++aU++OADFRYWqn79+m77n52dre+++05r1671u32dOnWsxP/f//53QPsmSdddd502bNigvLw8bdq0SfXq1dPQoUO9LmjmyZl0HDp0yOuxgwcPKiwsTNWrV3cr90xmTp8+rdzcXLcEplOnTlqxYoXy8vK0ZcsWdejQQePGjdOiRYtKHE/37t116tQpbd68WRs2bFDPnj2t8nXr1lm/1fZMKEPNV0KXk5Mjh8OhmJgYVa9eXWFhYX7nVZJq1qwp6cxV5ydPnqw9e/YoJydHs2fP1pYtWwL69jQ9Pd3t+fPCCy8ENE7JO+H0vAZBaWvDOf7ExERJ0oEDB/yOs3r16rLb7Ro5cqTXc955cybcVapU0T333KNPP/1UR44cUWZmpvbv369evXpZZ1fUrFlTM2bMUHZ2tr799ltNmTJFS5cu9fr2vTyqV68um83m9vtyp0ASeQAAiTYABF1RUZEWLFiglJQUbdy40es2YcIEHTp0SKtXr5bD4bC+eXbeXE/p9OfIkSO68847FR4ervHjxwc0Luffj3YmBb6SB+lMApGfn2996xgeHq6JEydqz549+utf/+pzmx9++EEffvihpLMXQfvf//1fr31/5513FB4eXurVrSdOnKgaNWrogQceCGjfXEVERKhz586aOnWqpDN/L9xZLsnrG9smTZqobt26eu2119w+SDh27JjefPNN60rkrjz/fvnrr7+u06dP+/zQw263q3379taVpT1PV/bUrl07xcXFacaMGcrJybES7R49emj79u16/fXX1bx5c59nMnjOg6/9DZalS5eqsLDQul9QUKAVK1aoU6dOstvtqlq1qtq3b6+lS5e6jaG4uFgLFy5UvXr1dNlll3m1W7t2bY0cOVJDhgzRF198YSWW/vanYcOGbs+fJk2auD3++eef67PPPnMre+211xQbG+t2cThfOnTooKioKC1cuNCt/MCBA3r33XetDzsuu+wypaSkaO7cuTp58qTPtqKjo9W1a1dt375daWlpXs/7Nm3a+PymuVq1arrhhhs0ZswYHTlyxOeV1uvXr6+77rpLPXv2LHV9BaJq1apq06aNli9f7nbxvp9//tnn1ckBAN4u3D8OCQCV1OrVq3Xw4EFNnTrVZ+KVmpqqWbNmKSMjQ/369Su1va+++kpbtmxRcXGxcnNztXXrVmVkZCg/P18vv/yyWrRo4bXNzp07rStu5+bmaunSpVq3bp0GDBigRo0aSZLuuOMOHT16VIMGDVJqaqrsdrv27NmjZ555RmFhYZo4caLV3n333afdu3dr0qRJ+vjjjzV06FAlJycrLy9Pmzdv1osvvqjJkyerffv2evnll9WsWTPrit6e+vfvr7fffluHDx+2kn5PcXFxeuihhwL+EOHRRx/VgQMH1L17d9WrV09Hjx7VzJkzFR4ers6dO0s6czp3VFSUXn31VTVr1kwxMTFKSkpSUlKSnnzySd18883q16+f/vjHP+rkyZOaNm2ajh49qieeeMKrv6VLl6pKlSrq2bOnPv/8cz3yyCNq2bKlbrrpJknSnDlz9O6776pv376qX7++CgsLrQ8XSjvl2263q3PnzlqxYoUaNWpk/S3kq666ShEREdqwYYPuvvvuUufE+bfVZ86cqREjRig8PFxNmjQJ6IOcQNjtdvXs2VP33HOPiouLNXXqVOXn57tdPXvKlCnq2bOnunbtqnvvvVcOh0PPP/+8du7cqczMTOsb5Pbt26tfv35KS0tT9erVtXv3br3yyituH3I492fq1Knq3bu37Ha70tLS3H5D7EtSUpKuvfZapaenq06dOlq4cKHWrVunqVOnen2A4qlatWp65JFH9Je//EXDhw/XkCFDlJubq8mTJysyMlKTJk2y6j733HPq37+/rrzySo0fP17169fXvn37tHbtWuuDmZkzZ+p3v/udOnXqpP/5n/9Rw4YNVVBQoK+//lorVqzQu+++K+nMcyQ1NVVt2rRRYmKivv32W82YMUMNGjTQpZdeqry8PHXt2lVDhw5V06ZNFRsbq23btllXCg+Gxx57TH379lWvXr305z//WUVFRZo2bZpiYmLK/FcBAOCidN4uwwYAvwK+rjp+/fXXG4fD4Xblbk+DBw82VapUMTk5OX7rOK+E7LxVqVLFJCQkmA4dOpi//OUvJjs722sbX1cdj4+PN1dccYWZPn26ddVqY4xZu3atGTVqlGnevLmJj483VapUMXXq1DEDBw40WVlZPsf01ltvmb59+5rExERTpUoVU716ddO1a1czZ84cc/LkSbN8+XIjycyYMcPvfq1Zs8ZIMk8//bTfOTTGmJMnT5pGjRoFdNXxlStXmt69e5u6desah8NhatWqZfr06WPef/99t3qZmZmmadOmJjw83EgykyZNsh5bvny5ad++vYmMjDRVq1Y13bt3Nx9++KHb9s6rjv/rX/8y/fv3NzExMSY2NtYMGTLEfP/991a9rKwsM2DAANOgQQMTERFhEhISTOfOnc3bb79d4n44zZw500gyt99+u1t5z549jSSvdnxdddwYYx588EGTlJRkwsLCjCSzceNGY8yZq3H37dvXq9/OnTubzp07lzg2Z19Tp041kydPNvXq1TMOh8P89re/NWvXrvWq//7775tu3bqZqlWrmqioKHPllVeaFStWuNV54IEHTJs2bUz16tVNRESEady4sRk/frz58ccfrTonT540t912m0lMTDQ2m83vFdVdOffzjTfeMC1atDAOh8M0bNjQTJ8+3a1eaVf4f+mll0xaWppxOBwmPj7eXHfddebzzz/3qpeVlWV69+5t4uPjTUREhElJSfG6Qv3evXvNqFGjTN26dU14eLhJTEw0HTt2NI8//rhV5+mnnzYdO3Y0NWvWNA6Hw9SvX9+MHj3aes4XFhaaO++806SlpZm4uDgTFRVl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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "execution_count": 2,
+ "id": "df942a2c",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-11-24T18:33:05.816985Z",
+ "iopub.status.busy": "2025-11-24T18:33:05.816985Z",
+ "iopub.status.idle": "2025-11-24T18:33:06.001663Z",
+ "shell.execute_reply": "2025-11-24T18:33:06.001511Z"
}
- ],
+ },
+ "outputs": [],
"source": [
- "fig, ax_barcode = plt.subplots(figsize=(10,1.5))\n",
+ "fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),\n",
+ " gridspec_kw={'height_ratios':[10,1]})\n",
+ "\n",
+ "gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')\n",
+ "city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')\n",
+ "\n",
+ "plot_stops(stops_tadb, ax=ax_map, cmap='Reds')\n",
+ "plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)\n",
+ "ax_map.set_axis_off()\n",
"\n",
"plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)\n",
- "plot_stops_barcode(stops_tadb, ax=ax_barcode, stop_color='red', set_xlim=False, timestamp='unix_ts')\n",
- "plt.title(\"TA-DBSCAN stops with post-processing\")\n",
- "plt.tight_layout()\n",
+ "plot_stops_barcode(stops_tadb, ax=ax_barcode, cmap='Reds', set_xlim=False, timestamp='unix_ts')\n",
+ "\n",
+ "plt.tight_layout(pad=0.1)\n",
"plt.show()"
]
}
@@ -117,7 +119,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.5"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/examples/tadbscan_demo.py b/examples/tadbscan_demo.py
index 9e63e671..7b6eb0d3 100644
--- a/examples/tadbscan_demo.py
+++ b/examples/tadbscan_demo.py
@@ -26,46 +26,52 @@
# %%
# %matplotlib inline
+import matplotlib
+matplotlib.use('TkAgg')
+import matplotlib.pyplot as plt
+plt.ion()
# Imports
import nomad.io.base as loader
import geopandas as gpd
from shapely.geometry import box
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode
+from nomad.stop_detection.viz import plot_stops_barcode, plot_time_barcode, plot_stops, plot_pings
import nomad.stop_detection.dbscan as DBSCAN
-import nomad.filters as filters
-import nomad.stop_detection.postprocessing as post
# Load data
-from nomad.city_gen import City
-city_obj = City.from_geopackage("garden-city.gpkg")
-# Create a simple bounds box for visualization
-outer_box = box(0, 0, city_obj.dimensions[0], city_obj.dimensions[1])
+import nomad.data as data_folder
+from pathlib import Path
+data_dir = Path(data_folder.__file__).parent
+city = gpd.read_parquet(data_dir / 'garden-city-buildings-mercator.parquet')
+outer_box = box(*city.total_bounds)
filepath_root = 'gc_data_long/'
tc = {"user_id": "gc_identifier", "x": "dev_x", "y": "dev_y", "timestamp": "unix_ts"}
# Density based stop detection (Temporal DBSCAN)
-users = ['confident_aryabhata']
-traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters = ('date','<=', '2024-01-03'), traj_cols=tc)
+users = ['admiring_brattain']
+traj = loader.sample_from_file(filepath_root, format='parquet', users=users, filters=('date','==', '2024-01-01'), traj_cols=tc)
-user_data_tadb = traj.assign(cluster=DBSCAN.ta_dbscan_labels(traj, time_thresh=240, dist_thresh=15, min_pts=3, traj_cols=tc))
stops_tadb = DBSCAN.ta_dbscan(traj,
time_thresh=720,
dist_thresh=15,
min_pts=3,
complete_output=True,
- traj_cols=tc)
-stops_tadb["cluster"] = post.remove_overlaps(user_data_tadb, time_thresh=240, method='cluster', traj_cols=tc, min_pts=3, dur_min=5, min_cluster_size=3)
+ traj_cols=tc)
# %%
-fig, ax_barcode = plt.subplots(figsize=(10,1.5))
+fig, (ax_map, ax_barcode) = plt.subplots(2, 1, figsize=(6,6.5),
+ gridspec_kw={'height_ratios':[10,1]})
+
+gpd.GeoDataFrame(geometry=[outer_box], crs='EPSG:3857').plot(ax=ax_map, color='#d3d3d3')
+city.plot(ax=ax_map, edgecolor='white', linewidth=1, color='#8c8c8c')
+
+plot_stops(stops_tadb, ax=ax_map, cmap='Reds')
+plot_pings(traj, ax=ax_map, s=6, color='black', alpha=0.5, traj_cols=tc)
+ax_map.set_axis_off()
plot_time_barcode(traj['unix_ts'], ax=ax_barcode, set_xlim=True)
-plot_stops_barcode(stops_tadb, ax=ax_barcode, stop_color='red', set_xlim=False, timestamp='unix_ts')
-plt.title("TA-DBSCAN stops with post-processing")
-plt.tight_layout()
+plot_stops_barcode(stops_tadb, ax=ax_barcode, cmap='Reds', set_xlim=False, timestamp='unix_ts')
+
+plt.tight_layout(pad=0.1)
plt.show()
diff --git a/nomad/data/garden-city-buildings-mercator.parquet b/nomad/data/garden-city-buildings-mercator.parquet
new file mode 100644
index 00000000..aceac951
Binary files /dev/null and b/nomad/data/garden-city-buildings-mercator.parquet differ
diff --git a/nomad/data/garden-city-buildings.geojson b/nomad/data/garden-city-buildings.geojson
new file mode 100644
index 00000000..41979fd3
--- /dev/null
+++ b/nomad/data/garden-city-buildings.geojson
@@ -0,0 +1,113 @@
+{
+"type": "FeatureCollection",
+"name": "garden-city-buildings",
+"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
+"features": [
+{ "type": "Feature", "properties": { "index": "p-x12-y11", "id": "p-x12-y11", "building_type": "park", "door_cell_x": 13, "door_cell_y": 11, "size": 16, "door_point_x": 13.0, "door_point_y": 11.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317674376729549, 36.669298771960719 ], [ -38.317674376729549, 36.669731091674187 ], [ -38.318213365900021, 36.669731091674187 ], [ -38.318213365900021, 36.669298771960719 ], [ -38.317674376729549, 36.669298771960719 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x7-y8", "id": "h-x7-y8", "building_type": "home", "door_cell_x": 8, "door_cell_y": 8, "size": 2, "door_point_x": 8.0, "door_point_y": 8.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318482860485254, 36.669082611193211 ], [ -38.318348113192641, 36.669082611193211 ], [ -38.318348113192641, 36.669298771960719 ], [ -38.318482860485254, 36.669298771960719 ], [ -38.318482860485254, 36.669082611193211 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x9-y7", "id": "h-x9-y7", "building_type": "home", "door_cell_x": 9, "door_cell_y": 8, "size": 2, "door_point_x": 9.5, "door_point_y": 8.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318348113192641, 36.669082611193211 ], [ -38.318078618607409, 36.669082611193211 ], [ -38.318078618607409, 36.669190691652865 ], [ -38.318348113192641, 36.669190691652865 ], [ -38.318348113192641, 36.669082611193211 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x10-y7", "id": "h-x10-y7", "building_type": "home", "door_cell_x": 10, "door_cell_y": 8, "size": 1, "door_point_x": 10.5, "door_point_y": 8.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317943871314782, 36.669082611193211 ], [ -38.317943871314782, 36.669190691652865 ], [ -38.318078618607409, 36.669190691652865 ], [ -38.318078618607409, 36.669082611193211 ], [ -38.317943871314782, 36.669082611193211 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x11-y7", "id": "h-x11-y7", "building_type": "home", "door_cell_x": 11, "door_cell_y": 8, "size": 1, "door_point_x": 11.5, "door_point_y": 8.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317809124022169, 36.669082611193211 ], [ -38.317809124022169, 36.669190691652865 ], [ -38.317943871314782, 36.669190691652865 ], [ -38.317943871314782, 36.669082611193211 ], [ -38.317809124022169, 36.669082611193211 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x13-y7", "id": "h-x13-y7", "building_type": "home", "door_cell_x": 13, "door_cell_y": 6, "size": 1, "door_point_x": 13.5, "door_point_y": 7.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317539629436936, 36.669082611193211 ], [ -38.317539629436936, 36.669190691652865 ], [ -38.317674376729549, 36.669190691652865 ], [ -38.317674376729549, 36.669082611193211 ], [ -38.317539629436936, 36.669082611193211 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x14-y7", "id": "h-x14-y7", "building_type": "home", "door_cell_x": 14, "door_cell_y": 6, "size": 1, "door_point_x": 14.5, "door_point_y": 7.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317404882144309, 36.669082611193211 ], [ -38.317404882144309, 36.669190691652865 ], [ -38.317539629436936, 36.669190691652865 ], [ -38.317539629436936, 36.669082611193211 ], [ -38.317404882144309, 36.669082611193211 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x14-y8", "id": "h-x14-y8", "building_type": "home", "door_cell_x": 13, "door_cell_y": 8, "size": 1, "door_point_x": 14.0, "door_point_y": 8.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317404882144309, 36.669190691652865 ], [ -38.317404882144309, 36.669298771960719 ], [ -38.317539629436936, 36.669298771960719 ], [ -38.317539629436936, 36.669190691652865 ], [ -38.317404882144309, 36.669190691652865 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x14-y9", "id": "h-x14-y9", "building_type": "home", "door_cell_x": 13, "door_cell_y": 9, "size": 1, "door_point_x": 14.0, "door_point_y": 9.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317404882144309, 36.669298771960719 ], [ -38.317404882144309, 36.669406852116779 ], [ -38.317539629436936, 36.669406852116779 ], [ -38.317539629436936, 36.669298771960719 ], [ -38.317404882144309, 36.669298771960719 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x14-y11", "id": "h-x14-y11", "building_type": "home", "door_cell_x": 13, "door_cell_y": 11, "size": 1, "door_point_x": 14.0, "door_point_y": 11.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317404882144309, 36.669514932121047 ], [ -38.317404882144309, 36.669623011973513 ], [ -38.317539629436936, 36.669623011973513 ], [ -38.317539629436936, 36.669514932121047 ], [ -38.317404882144309, 36.669514932121047 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x14-y12", "id": "h-x14-y12", "building_type": "home", "door_cell_x": 13, "door_cell_y": 12, "size": 1, "door_point_x": 14.0, "door_point_y": 12.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317404882144309, 36.669623011973513 ], [ -38.317404882144309, 36.669731091674187 ], [ -38.317539629436936, 36.669731091674187 ], [ -38.317539629436936, 36.669623011973513 ], [ -38.317404882144309, 36.669623011973513 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x14-y13", "id": "h-x14-y13", "building_type": "home", "door_cell_x": 15, "door_cell_y": 13, "size": 1, "door_point_x": 15.0, "door_point_y": 13.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317404882144309, 36.669731091674187 ], [ -38.317404882144309, 36.669839171223067 ], [ -38.317539629436936, 36.669839171223067 ], [ -38.317539629436936, 36.669731091674187 ], [ -38.317404882144309, 36.669731091674187 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x13-y14", "id": "h-x13-y14", "building_type": "home", "door_cell_x": 13, "door_cell_y": 13, "size": 2, "door_point_x": 13.5, "door_point_y": 14.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317674376729549, 36.669839171223067 ], [ -38.317404882144309, 36.669839171223067 ], [ -38.317404882144309, 36.66994725062014 ], [ -38.317674376729549, 36.66994725062014 ], [ -38.317674376729549, 36.669839171223067 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x12-y14", "id": "h-x12-y14", "building_type": "home", "door_cell_x": 12, "door_cell_y": 13, "size": 1, "door_point_x": 12.5, "door_point_y": 14.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317674376729549, 36.669839171223067 ], [ -38.317674376729549, 36.66994725062014 ], [ -38.317809124022169, 36.66994725062014 ], [ -38.317809124022169, 36.669839171223067 ], [ -38.317674376729549, 36.669839171223067 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x11-y14", "id": "h-x11-y14", "building_type": "home", "door_cell_x": 11, "door_cell_y": 13, "size": 1, "door_point_x": 11.5, "door_point_y": 14.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317809124022169, 36.669839171223067 ], [ -38.317809124022169, 36.66994725062014 ], [ -38.317943871314782, 36.66994725062014 ], [ -38.317943871314782, 36.669839171223067 ], [ -38.317809124022169, 36.669839171223067 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x9-y14", "id": "h-x9-y14", "building_type": "home", "door_cell_x": 9, "door_cell_y": 13, "size": 1, "door_point_x": 9.5, "door_point_y": 14.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318078618607409, 36.669839171223067 ], [ -38.318078618607409, 36.66994725062014 ], [ -38.318213365900021, 36.66994725062014 ], [ -38.318213365900021, 36.669839171223067 ], [ -38.318078618607409, 36.669839171223067 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x8-y14", "id": "h-x8-y14", "building_type": "home", "door_cell_x": 8, "door_cell_y": 13, "size": 1, "door_point_x": 8.5, "door_point_y": 14.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318213365900021, 36.669839171223067 ], [ -38.318213365900021, 36.66994725062014 ], [ -38.318348113192641, 36.66994725062014 ], [ -38.318348113192641, 36.669839171223067 ], [ -38.318213365900021, 36.669839171223067 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x7-y14", "id": "h-x7-y14", "building_type": "home", "door_cell_x": 7, "door_cell_y": 15, "size": 1, "door_point_x": 7.5, "door_point_y": 15.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318348113192641, 36.669839171223067 ], [ -38.318348113192641, 36.66994725062014 ], [ -38.318482860485254, 36.66994725062014 ], [ -38.318482860485254, 36.669839171223067 ], [ -38.318348113192641, 36.669839171223067 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x7-y13", "id": "h-x7-y13", "building_type": "home", "door_cell_x": 6, "door_cell_y": 13, "size": 1, "door_point_x": 7.0, "door_point_y": 13.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318348113192641, 36.669731091674187 ], [ -38.318348113192641, 36.669839171223067 ], [ -38.318482860485254, 36.669839171223067 ], [ -38.318482860485254, 36.669731091674187 ], [ -38.318348113192641, 36.669731091674187 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x7-y12", "id": "h-x7-y12", "building_type": "home", "door_cell_x": 8, "door_cell_y": 12, "size": 1, "door_point_x": 8.0, "door_point_y": 12.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318348113192641, 36.669623011973513 ], [ -38.318348113192641, 36.669731091674187 ], [ -38.318482860485254, 36.669731091674187 ], [ -38.318482860485254, 36.669623011973513 ], [ -38.318348113192641, 36.669623011973513 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "h-x7-y10", "id": "h-x7-y10", "building_type": "home", "door_cell_x": 8, "door_cell_y": 10, "size": 2, "door_point_x": 8.0, "door_point_y": 10.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318482860485254, 36.669298771960719 ], [ -38.318348113192641, 36.669298771960719 ], [ -38.318348113192641, 36.669514932121047 ], [ -38.318482860485254, 36.669514932121047 ], [ -38.318482860485254, 36.669298771960719 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x4-y4", "id": "w-x4-y4", "building_type": "workplace", "door_cell_x": 3, "door_cell_y": 4, "size": 2, "door_point_x": 4.0, "door_point_y": 4.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318887102363114, 36.668758368903482 ], [ -38.318752355070494, 36.668758368903482 ], [ -38.318752355070494, 36.668974530581764 ], [ -38.318887102363114, 36.668974530581764 ], [ -38.318887102363114, 36.668758368903482 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x5-y4", "id": "w-x5-y4", "building_type": "workplace", "door_cell_x": 5, "door_cell_y": 3, "size": 2, "door_point_x": 5.5, "door_point_y": 4.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318752355070494, 36.668758368903482 ], [ -38.318617607777881, 36.668758368903482 ], [ -38.318617607777881, 36.668974530581764 ], [ -38.318752355070494, 36.668974530581764 ], [ -38.318752355070494, 36.668758368903482 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x6-y5", "id": "w-x6-y5", "building_type": "workplace", "door_cell_x": 6, "door_cell_y": 6, "size": 4, "door_point_x": 6.5, "door_point_y": 6.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318348113192641, 36.668758368903482 ], [ -38.318348113192641, 36.668974530581764 ], [ -38.318617607777881, 36.668974530581764 ], [ -38.318617607777881, 36.668758368903482 ], [ -38.318348113192641, 36.668758368903482 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x8-y5", "id": "w-x8-y5", "building_type": "workplace", "door_cell_x": 8, "door_cell_y": 6, "size": 4, "door_point_x": 8.5, "door_point_y": 6.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318078618607409, 36.668758368903482 ], [ -38.318078618607409, 36.668974530581764 ], [ -38.318348113192641, 36.668974530581764 ], [ -38.318348113192641, 36.668758368903482 ], [ -38.318078618607409, 36.668758368903482 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x12-y5", "id": "w-x12-y5", "building_type": "workplace", "door_cell_x": 12, "door_cell_y": 6, "size": 3, "door_point_x": 12.5, "door_point_y": 6.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317539629436936, 36.668866449818516 ], [ -38.317539629436936, 36.668974530581764 ], [ -38.317943871314782, 36.668974530581764 ], [ -38.317943871314782, 36.668866449818516 ], [ -38.317539629436936, 36.668866449818516 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x12-y4", "id": "w-x12-y4", "building_type": "workplace", "door_cell_x": 12, "door_cell_y": 3, "size": 3, "door_point_x": 12.5, "door_point_y": 4.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317539629436936, 36.668758368903482 ], [ -38.317539629436936, 36.668866449818516 ], [ -38.317943871314782, 36.668866449818516 ], [ -38.317943871314782, 36.668758368903482 ], [ -38.317539629436936, 36.668758368903482 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x15-y4", "id": "w-x15-y4", "building_type": "workplace", "door_cell_x": 15, "door_cell_y": 3, "size": 6, "door_point_x": 15.5, "door_point_y": 4.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317135387559077, 36.668758368903482 ], [ -38.317135387559077, 36.668974530581764 ], [ -38.317539629436936, 36.668974530581764 ], [ -38.317539629436936, 36.668758368903482 ], [ -38.317135387559077, 36.668758368903482 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x17-y4", "id": "w-x17-y4", "building_type": "workplace", "door_cell_x": 18, "door_cell_y": 4, "size": 2, "door_point_x": 18.0, "door_point_y": 4.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317000640266464, 36.668758368903482 ], [ -38.317000640266464, 36.668974530581764 ], [ -38.317135387559077, 36.668974530581764 ], [ -38.317135387559077, 36.668758368903482 ], [ -38.317000640266464, 36.668758368903482 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x17-y6", "id": "w-x17-y6", "building_type": "workplace", "door_cell_x": 18, "door_cell_y": 6, "size": 4, "door_point_x": 18.0, "door_point_y": 6.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317000640266464, 36.668974530581764 ], [ -38.317000640266464, 36.669190691652865 ], [ -38.317270134851697, 36.669190691652865 ], [ -38.317270134851697, 36.668974530581764 ], [ -38.317000640266464, 36.668974530581764 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x16-y9", "id": "w-x16-y9", "building_type": "workplace", "door_cell_x": 15, "door_cell_y": 9, "size": 2, "door_point_x": 16.0, "door_point_y": 9.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317135387559077, 36.669190691652865 ], [ -38.317135387559077, 36.669406852116779 ], [ -38.317270134851697, 36.669406852116779 ], [ -38.317270134851697, 36.669190691652865 ], [ -38.317135387559077, 36.669190691652865 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x17-y8", "id": "w-x17-y8", "building_type": "workplace", "door_cell_x": 18, "door_cell_y": 8, "size": 2, "door_point_x": 18.0, "door_point_y": 8.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317000640266464, 36.669190691652865 ], [ -38.317000640266464, 36.669406852116779 ], [ -38.317135387559077, 36.669406852116779 ], [ -38.317135387559077, 36.669190691652865 ], [ -38.317000640266464, 36.669190691652865 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x17-y10", "id": "w-x17-y10", "building_type": "workplace", "door_cell_x": 18, "door_cell_y": 10, "size": 4, "door_point_x": 18.0, "door_point_y": 10.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317000640266464, 36.669406852116779 ], [ -38.317000640266464, 36.669623011973513 ], [ -38.317270134851697, 36.669623011973513 ], [ -38.317270134851697, 36.669406852116779 ], [ -38.317000640266464, 36.669406852116779 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x17-y13", "id": "w-x17-y13", "building_type": "workplace", "door_cell_x": 18, "door_cell_y": 13, "size": 4, "door_point_x": 18.0, "door_point_y": 13.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317000640266464, 36.669731091674187 ], [ -38.317000640266464, 36.66994725062014 ], [ -38.317270134851697, 36.66994725062014 ], [ -38.317270134851697, 36.669731091674187 ], [ -38.317000640266464, 36.669731091674187 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x17-y15", "id": "w-x17-y15", "building_type": "workplace", "door_cell_x": 18, "door_cell_y": 15, "size": 2, "door_point_x": 18.0, "door_point_y": 15.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317000640266464, 36.66994725062014 ], [ -38.317000640266464, 36.670055329865427 ], [ -38.317270134851697, 36.670055329865427 ], [ -38.317270134851697, 36.66994725062014 ], [ -38.317000640266464, 36.66994725062014 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x15-y16", "id": "w-x15-y16", "building_type": "workplace", "door_cell_x": 15, "door_cell_y": 15, "size": 3, "door_point_x": 15.5, "door_point_y": 16.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317000640266464, 36.670055329865427 ], [ -38.317000640266464, 36.670163408958906 ], [ -38.317404882144309, 36.670163408958906 ], [ -38.317404882144309, 36.670055329865427 ], [ -38.317000640266464, 36.670055329865427 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x14-y16", "id": "w-x14-y16", "building_type": "workplace", "door_cell_x": 14, "door_cell_y": 15, "size": 1, "door_point_x": 14.5, "door_point_y": 16.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317404882144309, 36.670055329865427 ], [ -38.317404882144309, 36.670163408958906 ], [ -38.317539629436936, 36.670163408958906 ], [ -38.317539629436936, 36.670055329865427 ], [ -38.317404882144309, 36.670055329865427 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x16-y17", "id": "w-x16-y17", "building_type": "workplace", "door_cell_x": 16, "door_cell_y": 18, "size": 2, "door_point_x": 16.5, "door_point_y": 18.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317000640266464, 36.670163408958906 ], [ -38.317000640266464, 36.670271487900592 ], [ -38.317270134851697, 36.670271487900592 ], [ -38.317270134851697, 36.670163408958906 ], [ -38.317000640266464, 36.670163408958906 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x15-y17", "id": "w-x15-y17", "building_type": "workplace", "door_cell_x": 15, "door_cell_y": 18, "size": 2, "door_point_x": 15.5, "door_point_y": 18.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317270134851697, 36.670163408958906 ], [ -38.317270134851697, 36.670271487900592 ], [ -38.317539629436936, 36.670271487900592 ], [ -38.317539629436936, 36.670163408958906 ], [ -38.317270134851697, 36.670163408958906 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x13-y17", "id": "w-x13-y17", "building_type": "workplace", "door_cell_x": 13, "door_cell_y": 18, "size": 4, "door_point_x": 13.5, "door_point_y": 18.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317539629436936, 36.670055329865427 ], [ -38.317539629436936, 36.670271487900592 ], [ -38.317809124022169, 36.670271487900592 ], [ -38.317809124022169, 36.670055329865427 ], [ -38.317539629436936, 36.670055329865427 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x11-y17", "id": "w-x11-y17", "building_type": "workplace", "door_cell_x": 11, "door_cell_y": 18, "size": 2, "door_point_x": 11.5, "door_point_y": 18.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317809124022169, 36.670163408958906 ], [ -38.317809124022169, 36.670271487900592 ], [ -38.318078618607409, 36.670271487900592 ], [ -38.318078618607409, 36.670163408958906 ], [ -38.317809124022169, 36.670163408958906 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x11-y16", "id": "w-x11-y16", "building_type": "workplace", "door_cell_x": 11, "door_cell_y": 15, "size": 2, "door_point_x": 11.5, "door_point_y": 16.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317809124022169, 36.670055329865427 ], [ -38.317809124022169, 36.670163408958906 ], [ -38.318078618607409, 36.670163408958906 ], [ -38.318078618607409, 36.670055329865427 ], [ -38.317809124022169, 36.670055329865427 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x8-y17", "id": "w-x8-y17", "building_type": "workplace", "door_cell_x": 8, "door_cell_y": 18, "size": 4, "door_point_x": 8.5, "door_point_y": 18.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318213365900021, 36.670055329865427 ], [ -38.318213365900021, 36.670271487900592 ], [ -38.318482860485254, 36.670271487900592 ], [ -38.318482860485254, 36.670055329865427 ], [ -38.318213365900021, 36.670055329865427 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x6-y17", "id": "w-x6-y17", "building_type": "workplace", "door_cell_x": 6, "door_cell_y": 18, "size": 2, "door_point_x": 6.5, "door_point_y": 18.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318482860485254, 36.670163408958906 ], [ -38.318482860485254, 36.670271487900592 ], [ -38.318752355070494, 36.670271487900592 ], [ -38.318752355070494, 36.670163408958906 ], [ -38.318482860485254, 36.670163408958906 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x6-y16", "id": "w-x6-y16", "building_type": "workplace", "door_cell_x": 6, "door_cell_y": 15, "size": 2, "door_point_x": 6.5, "door_point_y": 16.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318482860485254, 36.670055329865427 ], [ -38.318482860485254, 36.670163408958906 ], [ -38.318752355070494, 36.670163408958906 ], [ -38.318752355070494, 36.670055329865427 ], [ -38.318482860485254, 36.670055329865427 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x4-y16", "id": "w-x4-y16", "building_type": "workplace", "door_cell_x": 3, "door_cell_y": 16, "size": 2, "door_point_x": 4.0, "door_point_y": 16.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318887102363114, 36.670055329865427 ], [ -38.318752355070494, 36.670055329865427 ], [ -38.318752355070494, 36.670271487900592 ], [ -38.318887102363114, 36.670271487900592 ], [ -38.318887102363114, 36.670055329865427 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x4-y13", "id": "w-x4-y13", "building_type": "workplace", "door_cell_x": 3, "door_cell_y": 13, "size": 6, "door_point_x": 4.0, "door_point_y": 13.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318617607777881, 36.669731091674187 ], [ -38.318617607777881, 36.670055329865427 ], [ -38.318887102363114, 36.670055329865427 ], [ -38.318887102363114, 36.669731091674187 ], [ -38.318617607777881, 36.669731091674187 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x5-y12", "id": "w-x5-y12", "building_type": "workplace", "door_cell_x": 6, "door_cell_y": 12, "size": 2, "door_point_x": 6.0, "door_point_y": 12.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318617607777881, 36.669623011973513 ], [ -38.318617607777881, 36.669731091674187 ], [ -38.318887102363114, 36.669731091674187 ], [ -38.318887102363114, 36.669623011973513 ], [ -38.318617607777881, 36.669623011973513 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x4-y10", "id": "w-x4-y10", "building_type": "workplace", "door_cell_x": 3, "door_cell_y": 10, "size": 2, "door_point_x": 4.0, "door_point_y": 10.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318887102363114, 36.669298771960719 ], [ -38.318752355070494, 36.669298771960719 ], [ -38.318752355070494, 36.669514932121047 ], [ -38.318887102363114, 36.669514932121047 ], [ -38.318887102363114, 36.669298771960719 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x5-y9", "id": "w-x5-y9", "building_type": "workplace", "door_cell_x": 6, "door_cell_y": 9, "size": 2, "door_point_x": 6.0, "door_point_y": 9.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318752355070494, 36.669298771960719 ], [ -38.318617607777881, 36.669298771960719 ], [ -38.318617607777881, 36.669514932121047 ], [ -38.318752355070494, 36.669514932121047 ], [ -38.318752355070494, 36.669298771960719 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x5-y8", "id": "w-x5-y8", "building_type": "workplace", "door_cell_x": 6, "door_cell_y": 8, "size": 2, "door_point_x": 6.0, "door_point_y": 8.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318887102363114, 36.669190691652865 ], [ -38.318617607777881, 36.669190691652865 ], [ -38.318617607777881, 36.669298771960719 ], [ -38.318887102363114, 36.669298771960719 ], [ -38.318887102363114, 36.669190691652865 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "w-x4-y6", "id": "w-x4-y6", "building_type": "workplace", "door_cell_x": 3, "door_cell_y": 6, "size": 4, "door_point_x": 4.0, "door_point_y": 6.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318617607777881, 36.668974530581764 ], [ -38.318617607777881, 36.669190691652865 ], [ -38.318887102363114, 36.669190691652865 ], [ -38.318887102363114, 36.668974530581764 ], [ -38.318617607777881, 36.668974530581764 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x1-y1", "id": "r-x1-y1", "building_type": "retail", "door_cell_x": 0, "door_cell_y": 1, "size": 4, "door_point_x": 1.0, "door_point_y": 1.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.6684341252476 ], [ -38.319021849655726, 36.668650287836641 ], [ -38.319291344240966, 36.668650287836641 ], [ -38.319291344240966, 36.6684341252476 ], [ -38.319021849655726, 36.6684341252476 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x3-y1", "id": "r-x3-y1", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 0, "size": 4, "door_point_x": 3.5, "door_point_y": 1.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318752355070494, 36.6684341252476 ], [ -38.318752355070494, 36.668650287836641 ], [ -38.319021849655726, 36.668650287836641 ], [ -38.319021849655726, 36.6684341252476 ], [ -38.318752355070494, 36.6684341252476 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x5-y1", "id": "r-x5-y1", "building_type": "retail", "door_cell_x": 5, "door_cell_y": 0, "size": 1, "door_point_x": 5.5, "door_point_y": 1.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318617607777881, 36.6684341252476 ], [ -38.318617607777881, 36.668542206618028 ], [ -38.318752355070494, 36.668542206618028 ], [ -38.318752355070494, 36.6684341252476 ], [ -38.318617607777881, 36.6684341252476 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x5-y2", "id": "r-x5-y2", "building_type": "retail", "door_cell_x": 5, "door_cell_y": 3, "size": 1, "door_point_x": 5.5, "door_point_y": 3.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318617607777881, 36.668542206618028 ], [ -38.318617607777881, 36.668650287836641 ], [ -38.318752355070494, 36.668650287836641 ], [ -38.318752355070494, 36.668542206618028 ], [ -38.318617607777881, 36.668542206618028 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x6-y1", "id": "r-x6-y1", "building_type": "retail", "door_cell_x": 6, "door_cell_y": 0, "size": 2, "door_point_x": 6.5, "door_point_y": 1.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318348113192641, 36.6684341252476 ], [ -38.318348113192641, 36.668542206618028 ], [ -38.318617607777881, 36.668542206618028 ], [ -38.318617607777881, 36.6684341252476 ], [ -38.318348113192641, 36.6684341252476 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x6-y2", "id": "r-x6-y2", "building_type": "retail", "door_cell_x": 6, "door_cell_y": 3, "size": 2, "door_point_x": 6.5, "door_point_y": 3.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318348113192641, 36.668542206618028 ], [ -38.318348113192641, 36.668650287836641 ], [ -38.318617607777881, 36.668650287836641 ], [ -38.318617607777881, 36.668542206618028 ], [ -38.318348113192641, 36.668542206618028 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x9-y2", "id": "r-x9-y2", "building_type": "retail", "door_cell_x": 9, "door_cell_y": 3, "size": 2, "door_point_x": 9.5, "door_point_y": 3.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318078618607409, 36.6684341252476 ], [ -38.318078618607409, 36.668650287836641 ], [ -38.318213365900021, 36.668650287836641 ], [ -38.318213365900021, 36.6684341252476 ], [ -38.318078618607409, 36.6684341252476 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x12-y2", "id": "r-x12-y2", "building_type": "retail", "door_cell_x": 12, "door_cell_y": 3, "size": 6, "door_point_x": 12.5, "door_point_y": 3.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317674376729549, 36.6684341252476 ], [ -38.317674376729549, 36.668650287836641 ], [ -38.318078618607409, 36.668650287836641 ], [ -38.318078618607409, 36.6684341252476 ], [ -38.317674376729549, 36.6684341252476 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x14-y2", "id": "r-x14-y2", "building_type": "retail", "door_cell_x": 14, "door_cell_y": 3, "size": 4, "door_point_x": 14.5, "door_point_y": 3.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317404882144309, 36.6684341252476 ], [ -38.317404882144309, 36.668650287836641 ], [ -38.317674376729549, 36.668650287836641 ], [ -38.317674376729549, 36.6684341252476 ], [ -38.317404882144309, 36.6684341252476 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x15-y2", "id": "r-x15-y2", "building_type": "retail", "door_cell_x": 15, "door_cell_y": 3, "size": 1, "door_point_x": 15.5, "door_point_y": 3.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317270134851697, 36.668542206618028 ], [ -38.317270134851697, 36.668650287836641 ], [ -38.317404882144309, 36.668650287836641 ], [ -38.317404882144309, 36.668542206618028 ], [ -38.317270134851697, 36.668542206618028 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x16-y2", "id": "r-x16-y2", "building_type": "retail", "door_cell_x": 16, "door_cell_y": 3, "size": 1, "door_point_x": 16.5, "door_point_y": 3.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317135387559077, 36.668542206618028 ], [ -38.317135387559077, 36.668650287836641 ], [ -38.317270134851697, 36.668650287836641 ], [ -38.317270134851697, 36.668542206618028 ], [ -38.317135387559077, 36.668542206618028 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x15-y1", "id": "r-x15-y1", "building_type": "retail", "door_cell_x": 15, "door_cell_y": 0, "size": 1, "door_point_x": 15.5, "door_point_y": 1.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317270134851697, 36.6684341252476 ], [ -38.317270134851697, 36.668542206618028 ], [ -38.317404882144309, 36.668542206618028 ], [ -38.317404882144309, 36.6684341252476 ], [ -38.317270134851697, 36.6684341252476 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x16-y1", "id": "r-x16-y1", "building_type": "retail", "door_cell_x": 16, "door_cell_y": 0, "size": 1, "door_point_x": 16.5, "door_point_y": 1.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317135387559077, 36.6684341252476 ], [ -38.317135387559077, 36.668542206618028 ], [ -38.317270134851697, 36.668542206618028 ], [ -38.317270134851697, 36.6684341252476 ], [ -38.317135387559077, 36.6684341252476 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x17-y2", "id": "r-x17-y2", "building_type": "retail", "door_cell_x": 17, "door_cell_y": 3, "size": 2, "door_point_x": 17.5, "door_point_y": 3.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316865892973837, 36.668542206618028 ], [ -38.316865892973837, 36.668650287836641 ], [ -38.317135387559077, 36.668650287836641 ], [ -38.317135387559077, 36.668542206618028 ], [ -38.316865892973837, 36.668542206618028 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x18-y1", "id": "r-x18-y1", "building_type": "retail", "door_cell_x": 18, "door_cell_y": 0, "size": 2, "door_point_x": 18.5, "door_point_y": 1.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316865892973837, 36.6684341252476 ], [ -38.316865892973837, 36.668542206618028 ], [ -38.317135387559077, 36.668542206618028 ], [ -38.317135387559077, 36.6684341252476 ], [ -38.316865892973837, 36.6684341252476 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x19-y1", "id": "r-x19-y1", "building_type": "retail", "door_cell_x": 19, "door_cell_y": 0, "size": 2, "door_point_x": 19.5, "door_point_y": 1.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316596398388604, 36.6684341252476 ], [ -38.316596398388604, 36.668542206618028 ], [ -38.316865892973837, 36.668542206618028 ], [ -38.316865892973837, 36.6684341252476 ], [ -38.316596398388604, 36.6684341252476 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x19-y3", "id": "r-x19-y3", "building_type": "retail", "door_cell_x": 18, "door_cell_y": 3, "size": 4, "door_point_x": 19.0, "door_point_y": 3.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316596398388604, 36.668542206618028 ], [ -38.316596398388604, 36.668758368903482 ], [ -38.316865892973837, 36.668758368903482 ], [ -38.316865892973837, 36.668542206618028 ], [ -38.316596398388604, 36.668542206618028 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x19-y5", "id": "r-x19-y5", "building_type": "retail", "door_cell_x": 18, "door_cell_y": 5, "size": 4, "door_point_x": 19.0, "door_point_y": 5.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316596398388604, 36.668758368903482 ], [ -38.316596398388604, 36.668974530581764 ], [ -38.316865892973837, 36.668974530581764 ], [ -38.316865892973837, 36.668758368903482 ], [ -38.316596398388604, 36.668758368903482 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x19-y7", "id": "r-x19-y7", "building_type": "retail", "door_cell_x": 18, "door_cell_y": 7, "size": 2, "door_point_x": 19.0, "door_point_y": 7.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316731145681224, 36.668974530581764 ], [ -38.316731145681224, 36.669190691652865 ], [ -38.316865892973837, 36.669190691652865 ], [ -38.316865892973837, 36.668974530581764 ], [ -38.316731145681224, 36.668974530581764 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x20-y7", "id": "r-x20-y7", "building_type": "retail", "door_cell_x": 21, "door_cell_y": 7, "size": 2, "door_point_x": 21.0, "door_point_y": 7.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316596398388604, 36.668974530581764 ], [ -38.316596398388604, 36.669190691652865 ], [ -38.316731145681224, 36.669190691652865 ], [ -38.316731145681224, 36.668974530581764 ], [ -38.316596398388604, 36.668974530581764 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x19-y10", "id": "r-x19-y10", "building_type": "retail", "door_cell_x": 18, "door_cell_y": 10, "size": 4, "door_point_x": 19.0, "door_point_y": 10.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316596398388604, 36.669298771960719 ], [ -38.316596398388604, 36.669514932121047 ], [ -38.316865892973837, 36.669514932121047 ], [ -38.316865892973837, 36.669298771960719 ], [ -38.316596398388604, 36.669298771960719 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x19-y11", "id": "r-x19-y11", "building_type": "retail", "door_cell_x": 18, "door_cell_y": 11, "size": 4, "door_point_x": 19.0, "door_point_y": 11.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316596398388604, 36.669514932121047 ], [ -38.316596398388604, 36.669731091674187 ], [ -38.316865892973837, 36.669731091674187 ], [ -38.316865892973837, 36.669514932121047 ], [ -38.316596398388604, 36.669514932121047 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x19-y13", "id": "r-x19-y13", "building_type": "retail", "door_cell_x": 18, "door_cell_y": 13, "size": 2, "door_point_x": 19.0, "door_point_y": 13.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316731145681224, 36.669731091674187 ], [ -38.316731145681224, 36.66994725062014 ], [ -38.316865892973837, 36.66994725062014 ], [ -38.316865892973837, 36.669731091674187 ], [ -38.316731145681224, 36.669731091674187 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x20-y13", "id": "r-x20-y13", "building_type": "retail", "door_cell_x": 21, "door_cell_y": 13, "size": 2, "door_point_x": 21.0, "door_point_y": 13.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316596398388604, 36.669731091674187 ], [ -38.316596398388604, 36.66994725062014 ], [ -38.316731145681224, 36.66994725062014 ], [ -38.316731145681224, 36.669731091674187 ], [ -38.316596398388604, 36.669731091674187 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x20-y16", "id": "r-x20-y16", "building_type": "retail", "door_cell_x": 21, "door_cell_y": 16, "size": 4, "door_point_x": 21.0, "door_point_y": 16.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316596398388604, 36.66994725062014 ], [ -38.316596398388604, 36.670163408958906 ], [ -38.316865892973837, 36.670163408958906 ], [ -38.316865892973837, 36.66994725062014 ], [ -38.316596398388604, 36.66994725062014 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x20-y18", "id": "r-x20-y18", "building_type": "retail", "door_cell_x": 21, "door_cell_y": 18, "size": 4, "door_point_x": 21.0, "door_point_y": 18.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316596398388604, 36.670163408958906 ], [ -38.316596398388604, 36.670379566690485 ], [ -38.316865892973837, 36.670379566690485 ], [ -38.316865892973837, 36.670163408958906 ], [ -38.316596398388604, 36.670163408958906 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x20-y19", "id": "r-x20-y19", "building_type": "retail", "door_cell_x": 21, "door_cell_y": 19, "size": 2, "door_point_x": 21.0, "door_point_y": 19.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316596398388604, 36.670379566690485 ], [ -38.316596398388604, 36.670487645328578 ], [ -38.316865892973837, 36.670487645328578 ], [ -38.316865892973837, 36.670379566690485 ], [ -38.316596398388604, 36.670379566690485 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x20-y20", "id": "r-x20-y20", "building_type": "retail", "door_cell_x": 20, "door_cell_y": 21, "size": 2, "door_point_x": 20.5, "door_point_y": 21.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.316596398388604, 36.670487645328578 ], [ -38.316596398388604, 36.67059572381487 ], [ -38.316865892973837, 36.67059572381487 ], [ -38.316865892973837, 36.670487645328578 ], [ -38.316596398388604, 36.670487645328578 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x17-y19", "id": "r-x17-y19", "building_type": "retail", "door_cell_x": 17, "door_cell_y": 18, "size": 2, "door_point_x": 17.5, "door_point_y": 19.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317000640266464, 36.670379566690485 ], [ -38.317000640266464, 36.67059572381487 ], [ -38.317135387559077, 36.67059572381487 ], [ -38.317135387559077, 36.670379566690485 ], [ -38.317000640266464, 36.670379566690485 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x16-y19", "id": "r-x16-y19", "building_type": "retail", "door_cell_x": 16, "door_cell_y": 18, "size": 2, "door_point_x": 16.5, "door_point_y": 19.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317135387559077, 36.670379566690485 ], [ -38.317135387559077, 36.67059572381487 ], [ -38.317270134851697, 36.67059572381487 ], [ -38.317270134851697, 36.670379566690485 ], [ -38.317135387559077, 36.670379566690485 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x14-y19", "id": "r-x14-y19", "building_type": "retail", "door_cell_x": 14, "door_cell_y": 18, "size": 3, "door_point_x": 14.5, "door_point_y": 19.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317270134851697, 36.670379566690485 ], [ -38.317270134851697, 36.670487645328578 ], [ -38.317674376729549, 36.670487645328578 ], [ -38.317674376729549, 36.670379566690485 ], [ -38.317270134851697, 36.670379566690485 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x15-y20", "id": "r-x15-y20", "building_type": "retail", "door_cell_x": 15, "door_cell_y": 21, "size": 2, "door_point_x": 15.5, "door_point_y": 21.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317270134851697, 36.670487645328578 ], [ -38.317270134851697, 36.67059572381487 ], [ -38.317539629436936, 36.67059572381487 ], [ -38.317539629436936, 36.670487645328578 ], [ -38.317270134851697, 36.670487645328578 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x13-y20", "id": "r-x13-y20", "building_type": "retail", "door_cell_x": 13, "door_cell_y": 21, "size": 2, "door_point_x": 13.5, "door_point_y": 21.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317539629436936, 36.670487645328578 ], [ -38.317539629436936, 36.67059572381487 ], [ -38.317809124022169, 36.67059572381487 ], [ -38.317809124022169, 36.670487645328578 ], [ -38.317539629436936, 36.670487645328578 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x12-y19", "id": "r-x12-y19", "building_type": "retail", "door_cell_x": 12, "door_cell_y": 18, "size": 1, "door_point_x": 12.5, "door_point_y": 19.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317674376729549, 36.670379566690485 ], [ -38.317674376729549, 36.670487645328578 ], [ -38.317809124022169, 36.670487645328578 ], [ -38.317809124022169, 36.670379566690485 ], [ -38.317674376729549, 36.670379566690485 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x11-y19", "id": "r-x11-y19", "building_type": "retail", "door_cell_x": 11, "door_cell_y": 18, "size": 4, "door_point_x": 11.5, "door_point_y": 19.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.317809124022169, 36.670379566690485 ], [ -38.317809124022169, 36.67059572381487 ], [ -38.318078618607409, 36.67059572381487 ], [ -38.318078618607409, 36.670379566690485 ], [ -38.317809124022169, 36.670379566690485 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x9-y19", "id": "r-x9-y19", "building_type": "retail", "door_cell_x": 9, "door_cell_y": 18, "size": 2, "door_point_x": 9.5, "door_point_y": 19.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318078618607409, 36.670379566690485 ], [ -38.318078618607409, 36.670487645328578 ], [ -38.318348113192641, 36.670487645328578 ], [ -38.318348113192641, 36.670379566690485 ], [ -38.318078618607409, 36.670379566690485 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x9-y20", "id": "r-x9-y20", "building_type": "retail", "door_cell_x": 9, "door_cell_y": 21, "size": 2, "door_point_x": 9.5, "door_point_y": 21.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318078618607409, 36.670487645328578 ], [ -38.318078618607409, 36.67059572381487 ], [ -38.318348113192641, 36.67059572381487 ], [ -38.318348113192641, 36.670487645328578 ], [ -38.318078618607409, 36.670487645328578 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x6-y20", "id": "r-x6-y20", "building_type": "retail", "door_cell_x": 6, "door_cell_y": 21, "size": 4, "door_point_x": 6.5, "door_point_y": 21.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318482860485254, 36.670379566690485 ], [ -38.318482860485254, 36.67059572381487 ], [ -38.318752355070494, 36.67059572381487 ], [ -38.318752355070494, 36.670379566690485 ], [ -38.318482860485254, 36.670379566690485 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x4-y20", "id": "r-x4-y20", "building_type": "retail", "door_cell_x": 4, "door_cell_y": 21, "size": 2, "door_point_x": 4.5, "door_point_y": 21.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318752355070494, 36.670487645328578 ], [ -38.318752355070494, 36.67059572381487 ], [ -38.319021849655726, 36.67059572381487 ], [ -38.319021849655726, 36.670487645328578 ], [ -38.318752355070494, 36.670487645328578 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x4-y19", "id": "r-x4-y19", "building_type": "retail", "door_cell_x": 4, "door_cell_y": 18, "size": 2, "door_point_x": 4.5, "door_point_y": 19.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.318752355070494, 36.670379566690485 ], [ -38.318752355070494, 36.670487645328578 ], [ -38.319021849655726, 36.670487645328578 ], [ -38.319021849655726, 36.670379566690485 ], [ -38.318752355070494, 36.670379566690485 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y19", "id": "r-x2-y19", "building_type": "retail", "door_cell_x": 2, "door_cell_y": 18, "size": 2, "door_point_x": 2.5, "door_point_y": 19.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.670379566690485 ], [ -38.319021849655726, 36.67059572381487 ], [ -38.319156596948346, 36.67059572381487 ], [ -38.319156596948346, 36.670379566690485 ], [ -38.319021849655726, 36.670379566690485 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x1-y19", "id": "r-x1-y19", "building_type": "retail", "door_cell_x": 1, "door_cell_y": 18, "size": 2, "door_point_x": 1.5, "door_point_y": 19.0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319156596948346, 36.670379566690485 ], [ -38.319156596948346, 36.67059572381487 ], [ -38.319291344240966, 36.67059572381487 ], [ -38.319291344240966, 36.670379566690485 ], [ -38.319156596948346, 36.670379566690485 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y17", "id": "r-x2-y17", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 17, "size": 4, "door_point_x": 3.0, "door_point_y": 17.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.670055329865427 ], [ -38.319021849655726, 36.670271487900592 ], [ -38.319291344240966, 36.670271487900592 ], [ -38.319291344240966, 36.670055329865427 ], [ -38.319021849655726, 36.670055329865427 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y15", "id": "r-x2-y15", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 15, "size": 2, "door_point_x": 3.0, "door_point_y": 15.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.66994725062014 ], [ -38.319021849655726, 36.670055329865427 ], [ -38.319291344240966, 36.670055329865427 ], [ -38.319291344240966, 36.66994725062014 ], [ -38.319021849655726, 36.66994725062014 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y14", "id": "r-x2-y14", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 14, "size": 2, "door_point_x": 3.0, "door_point_y": 14.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.669839171223067 ], [ -38.319021849655726, 36.66994725062014 ], [ -38.319291344240966, 36.66994725062014 ], [ -38.319291344240966, 36.669839171223067 ], [ -38.319021849655726, 36.669839171223067 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y12", "id": "r-x2-y12", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 12, "size": 4, "door_point_x": 3.0, "door_point_y": 12.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.669623011973513 ], [ -38.319021849655726, 36.669839171223067 ], [ -38.319291344240966, 36.669839171223067 ], [ -38.319291344240966, 36.669623011973513 ], [ -38.319021849655726, 36.669623011973513 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y11", "id": "r-x2-y11", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 11, "size": 2, "door_point_x": 3.0, "door_point_y": 11.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.669514932121047 ], [ -38.319021849655726, 36.669623011973513 ], [ -38.319291344240966, 36.669623011973513 ], [ -38.319291344240966, 36.669514932121047 ], [ -38.319021849655726, 36.669514932121047 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y10", "id": "r-x2-y10", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 10, "size": 2, "door_point_x": 3.0, "door_point_y": 10.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.669406852116779 ], [ -38.319021849655726, 36.669514932121047 ], [ -38.319291344240966, 36.669514932121047 ], [ -38.319291344240966, 36.669406852116779 ], [ -38.319021849655726, 36.669406852116779 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y8", "id": "r-x2-y8", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 8, "size": 4, "door_point_x": 3.0, "door_point_y": 8.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.669190691652865 ], [ -38.319021849655726, 36.669406852116779 ], [ -38.319291344240966, 36.669406852116779 ], [ -38.319291344240966, 36.669190691652865 ], [ -38.319021849655726, 36.669190691652865 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y7", "id": "r-x2-y7", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 7, "size": 2, "door_point_x": 3.0, "door_point_y": 7.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.669082611193211 ], [ -38.319021849655726, 36.669190691652865 ], [ -38.319291344240966, 36.669190691652865 ], [ -38.319291344240966, 36.669082611193211 ], [ -38.319021849655726, 36.669082611193211 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x1-y5", "id": "r-x1-y5", "building_type": "retail", "door_cell_x": 0, "door_cell_y": 5, "size": 3, "door_point_x": 1.0, "door_point_y": 5.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319156596948346, 36.668758368903482 ], [ -38.319156596948346, 36.669082611193211 ], [ -38.319291344240966, 36.669082611193211 ], [ -38.319291344240966, 36.668758368903482 ], [ -38.319156596948346, 36.668758368903482 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y6", "id": "r-x2-y6", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 6, "size": 1, "door_point_x": 3.0, "door_point_y": 6.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.668974530581764 ], [ -38.319021849655726, 36.669082611193211 ], [ -38.319156596948346, 36.669082611193211 ], [ -38.319156596948346, 36.668974530581764 ], [ -38.319021849655726, 36.668974530581764 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y5", "id": "r-x2-y5", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 5, "size": 1, "door_point_x": 3.0, "door_point_y": 5.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.668866449818516 ], [ -38.319021849655726, 36.668974530581764 ], [ -38.319156596948346, 36.668974530581764 ], [ -38.319156596948346, 36.668866449818516 ], [ -38.319021849655726, 36.668866449818516 ] ] ] } },
+{ "type": "Feature", "properties": { "index": "r-x2-y4", "id": "r-x2-y4", "building_type": "retail", "door_cell_x": 3, "door_cell_y": 4, "size": 1, "door_point_x": 3.0, "door_point_y": 4.5 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -38.319021849655726, 36.668758368903482 ], [ -38.319021849655726, 36.668866449818516 ], [ -38.319156596948346, 36.668866449818516 ], [ -38.319156596948346, 36.668758368903482 ], [ -38.319021849655726, 36.668758368903482 ] ] ] } }
+]
+}
diff --git a/nomad/home_attribution.py b/nomad/home_attribution.py
index 90534184..074502d9 100644
--- a/nomad/home_attribution.py
+++ b/nomad/home_attribution.py
@@ -1,156 +1,156 @@
-import pandas as pd
-import nomad.io.base as loader
-from datetime import datetime, time, timedelta
-
-
-def nocturnal_stops(
- stops_table,
- dusk_hour=19,
- dawn_hour=6,
- start_datetime="start_datetime",
- end_datetime="end_datetime",
-):
- """Clip each stop to the nocturnal window between *dusk_hour* and *dawn_hour*.
-
- This helper assumes the caller already provides proper datetime columns. It
- merely slices the stop to the relevant night portion and recomputes the
- duration, dropping rows that do not intersect the night at all.
- """
-
- df = stops_table.copy()
-
- # Build candidate night windows for every stop
- df["_night_start"] = df.apply(
- lambda r: [
- pd.Timestamp(datetime.combine(d, time(dusk_hour)), tz=r[start_datetime].tzinfo)
- for d in pd.date_range(
- (r[start_datetime] - timedelta(days=1)).date(),
- r[end_datetime].date(),
- freq="D",
- )
- ],
- axis=1,
- )
-
- df = df.explode("_night_start", ignore_index=True)
- df["_night_end"] = df["_night_start"] + timedelta(hours=(24 - dusk_hour + dawn_hour))
-
- # Clip the stop to the nightly interval
- df[start_datetime] = df[[start_datetime, "_night_start"]].max(axis=1)
- df[end_datetime] = df[[end_datetime, "_night_end"]].min(axis=1)
-
- df["duration"] = (
- (df[end_datetime] - df[start_datetime]).dt.total_seconds() // 60
- ).astype(int)
-
- return df[df["duration"] > 0].drop(columns=["_night_start", "_night_end"])
-
-
-def compute_candidate_homes(
- stops_table,
- dusk_hour=19,
- dawn_hour=6,
- traj_cols=None,
- **kwargs,
-):
- """Aggregate nightly presence statistics for home inference.
-
- Column names are resolved through *traj_cols* or keyword overrides and no
- type coercion beyond what is strictly necessary for the calculation is
- performed.
- """
-
- stops = stops_table.copy()
-
- # Resolve column names
- traj_cols = loader._parse_traj_cols(stops.columns, traj_cols, kwargs)
- loader._has_time_cols(stops.columns, traj_cols)
-
- t_key, use_datetime = loader._fallback_time_cols_dt(stops.columns, traj_cols, kwargs)
- end_t_key = "end_datetime" if use_datetime else "end_timestamp"
-
- # Ensure we can compute an end time
- end_col_present = loader._has_end_cols(stops.columns, traj_cols)
- duration_col_present = loader._has_duration_cols(stops.columns, traj_cols)
- if not (end_col_present or duration_col_present):
- raise ValueError("stops_table must provide either an end time or a duration.")
-
- if not end_col_present:
- dur_col = traj_cols["duration"]
- if use_datetime:
- stops[end_t_key] = stops[traj_cols[t_key]] + pd.to_timedelta(stops[dur_col], unit="m")
- else:
- stops[end_t_key] = stops[traj_cols[t_key]] + stops[dur_col] * 60
-
- # Nocturnal clipping
- stops_night = nocturnal_stops(
- stops,
- dusk_hour=dusk_hour,
- dawn_hour=dawn_hour,
- start_datetime=traj_cols[t_key],
- end_datetime=end_t_key,
- )
-
- # Dates and ISO weeks (convert timestamps if needed)
- if use_datetime:
- dt = stops_night[traj_cols[t_key]]
- else:
- dt = pd.to_datetime(stops_night[traj_cols[t_key]], unit="s", utc=True)
-
- stops_night["_date"] = dt.dt.date
- stops_night["_iso_week"] = dt.dt.isocalendar().week
-
- out = (
- stops_night.groupby([traj_cols["user_id"], traj_cols["location_id"]], as_index=False)
- .agg(
- num_nights=("_date", "nunique"),
- num_weeks=("_iso_week", "nunique"),
- total_duration=(traj_cols["duration"], "sum"),
- )
- )
-
- return out
-
-
-
-def select_home(
- candidate_homes,
- stops_table,
- min_days,
- min_weeks,
- traj_cols=None,
- **kwargs,
-):
- """Select a single home location per user."""
-
- traj_cols = loader._parse_traj_cols(candidate_homes.columns, traj_cols, kwargs)
-
- # Last observation date
- t_key, use_datetime = loader._fallback_time_cols_dt(stops_table.columns, traj_cols, kwargs)
- dt_series = (
- stops_table[traj_cols[t_key]]
- if use_datetime
- else pd.to_datetime(stops_table[traj_cols[t_key]], unit="s", utc=True)
- )
- last_date = dt_series.dt.date.max()
-
- # Filter and rank
- filtered = (
- candidate_homes.loc[
- (candidate_homes["num_nights"] >= min_days)
- & (candidate_homes["num_weeks"] >= min_weeks)
- ]
- .sort_values(
- [traj_cols["user_id"], "num_nights", "total_duration"],
- ascending=[True, False, False],
- )
- )
-
- best = (
- filtered.drop_duplicates(traj_cols["user_id"], keep="first")
- .assign(home_date=last_date)
- .reset_index(drop=True)
- )
-
- return best[[traj_cols["user_id"], traj_cols["location_id"], "home_date"]]
-
+import pandas as pd
+import nomad.io.base as loader
+from datetime import datetime, time, timedelta
+
+
+def nocturnal_stops(
+ stops_table,
+ dusk_hour=19,
+ dawn_hour=6,
+ start_datetime="start_datetime",
+ end_datetime="end_datetime",
+):
+ """Clip each stop to the nocturnal window between *dusk_hour* and *dawn_hour*.
+
+ This helper assumes the caller already provides proper datetime columns. It
+ merely slices the stop to the relevant night portion and recomputes the
+ duration, dropping rows that do not intersect the night at all.
+ """
+
+ df = stops_table.copy()
+
+ # Build candidate night windows for every stop
+ df["_night_start"] = df.apply(
+ lambda r: [
+ pd.Timestamp(datetime.combine(d, time(dusk_hour)), tz=r[start_datetime].tzinfo)
+ for d in pd.date_range(
+ (r[start_datetime] - timedelta(days=1)).date(),
+ r[end_datetime].date(),
+ freq="D",
+ )
+ ],
+ axis=1,
+ )
+
+ df = df.explode("_night_start", ignore_index=True)
+ df["_night_end"] = df["_night_start"] + timedelta(hours=(24 - dusk_hour + dawn_hour))
+
+ # Clip the stop to the nightly interval
+ df[start_datetime] = df[[start_datetime, "_night_start"]].max(axis=1)
+ df[end_datetime] = df[[end_datetime, "_night_end"]].min(axis=1)
+
+ df["duration"] = (
+ (df[end_datetime] - df[start_datetime]).dt.total_seconds() // 60
+ ).astype(int)
+
+ return df[df["duration"] > 0].drop(columns=["_night_start", "_night_end"])
+
+
+def compute_candidate_homes(
+ stops_table,
+ dusk_hour=19,
+ dawn_hour=6,
+ traj_cols=None,
+ **kwargs,
+):
+ """Aggregate nightly presence statistics for home inference.
+
+ Column names are resolved through *traj_cols* or keyword overrides and no
+ type coercion beyond what is strictly necessary for the calculation is
+ performed.
+ """
+
+ stops = stops_table.copy()
+
+ # Resolve column names
+ traj_cols = loader._parse_traj_cols(stops.columns, traj_cols, kwargs)
+ loader._has_time_cols(stops.columns, traj_cols)
+
+ t_key, use_datetime = loader._fallback_time_cols_dt(stops.columns, traj_cols, kwargs)
+ end_t_key = "end_datetime" if use_datetime else "end_timestamp"
+
+ # Ensure we can compute an end time
+ end_col_present = loader._has_end_cols(stops.columns, traj_cols)
+ duration_col_present = loader._has_duration_cols(stops.columns, traj_cols)
+ if not (end_col_present or duration_col_present):
+ raise ValueError("stops_table must provide either an end time or a duration.")
+
+ if not end_col_present:
+ dur_col = traj_cols["duration"]
+ if use_datetime:
+ stops[end_t_key] = stops[traj_cols[t_key]] + pd.to_timedelta(stops[dur_col], unit="m")
+ else:
+ stops[end_t_key] = stops[traj_cols[t_key]] + stops[dur_col] * 60
+
+ # Nocturnal clipping
+ stops_night = nocturnal_stops(
+ stops,
+ dusk_hour=dusk_hour,
+ dawn_hour=dawn_hour,
+ start_datetime=traj_cols[t_key],
+ end_datetime=end_t_key,
+ )
+
+ # Dates and ISO weeks (convert timestamps if needed)
+ if use_datetime:
+ dt = stops_night[traj_cols[t_key]]
+ else:
+ dt = pd.to_datetime(stops_night[traj_cols[t_key]], unit="s", utc=True)
+
+ stops_night["_date"] = dt.dt.date
+ stops_night["_iso_week"] = dt.dt.isocalendar().week
+
+ out = (
+ stops_night.groupby([traj_cols["user_id"], traj_cols["location_id"]], as_index=False)
+ .agg(
+ num_nights=("_date", "nunique"),
+ num_weeks=("_iso_week", "nunique"),
+ total_duration=(traj_cols["duration"], "sum"),
+ )
+ )
+
+ return out
+
+
+
+def select_home(
+ candidate_homes,
+ stops_table,
+ min_days,
+ min_weeks,
+ traj_cols=None,
+ **kwargs,
+):
+ """Select a single home location per user."""
+
+ traj_cols = loader._parse_traj_cols(candidate_homes.columns, traj_cols, kwargs)
+
+ # Last observation date
+ t_key, use_datetime = loader._fallback_time_cols_dt(stops_table.columns, traj_cols, kwargs)
+ dt_series = (
+ stops_table[traj_cols[t_key]]
+ if use_datetime
+ else pd.to_datetime(stops_table[traj_cols[t_key]], unit="s", utc=True)
+ )
+ last_date = dt_series.dt.date.max()
+
+ # Filter and rank
+ filtered = (
+ candidate_homes.loc[
+ (candidate_homes["num_nights"] >= min_days)
+ & (candidate_homes["num_weeks"] >= min_weeks)
+ ]
+ .sort_values(
+ [traj_cols["user_id"], "num_nights", "total_duration"],
+ ascending=[True, False, False],
+ )
+ )
+
+ best = (
+ filtered.drop_duplicates(traj_cols["user_id"], keep="first")
+ .assign(home_date=last_date)
+ .reset_index(drop=True)
+ )
+
+ return best[[traj_cols["user_id"], traj_cols["location_id"], "home_date"]]
+
diff --git a/nomad/visit_attribution/visit_attribution.py b/nomad/visit_attribution/visit_attribution.py
index 0daf845f..26c1ba5a 100644
--- a/nomad/visit_attribution/visit_attribution.py
+++ b/nomad/visit_attribution/visit_attribution.py
@@ -1,441 +1,441 @@
-import geopandas as gpd
-import nomad.io.base as loader
-import nomad.constants as constants
-import warnings
-import pandas as pd
-import nomad.io.base as loader
-import pyproj
-import pdb
-
-# TO DO: change to stops_to_poi
-def point_in_polygon(data, poi_table, method='centroid', data_crs=None, max_distance=0,
- cluster_label=None, location_id=None, traj_cols=None, **kwargs):
- """
- Assign each stop or cluster of pings in `data` to a polygon in `poi_table`,
- either by the cluster’s centroid location or by the most frequent polygon hit.
-
- Parameters
- ----------
- data : pd.DataFrame or gpd.GeoDataFrame
- A table of pings (with optional stop/duration columns) or stops,
- indexed by observation or cluster.
- poi_table : gpd.GeoDataFrame
- Polygons to match against, with CRS set and optional ID column.
- method : {'centroid', 'majority'}, default 'centroid'
- ‘centroid’ uses each cluster’s mean point; ‘majority’ picks the polygon
- most often visited within each cluster (only for ping data).
- data_crs : str or pyproj.CRS, optional
- CRS for `data` when it is a plain DataFrame; ignored if `data` is a GeoDataFrame.
- max_distance : float, default 0
- Search radius for nearest‐neighbor fall-back; zero triggers strict
- point-in-polygon matching.
- cluster_label : str, optional
- Column name holding cluster IDs in ping data; inferred from `data` if absent.
- location_id : str, optional
- Column in `poi_table` containing the output ID; uses the GeoDataFrame index if None.
- traj_cols : list of str, optional
- Names of the coordinate columns in `data` when it is a DataFrame.
- **kwargs
- Passed through to `poi_map` or the trajectory-column parser.
-
- Returns
- -------
- pd.Series
- Indexed like `data`, giving the matched polygon ID for each stop or ping.
- Points or clusters that fall outside every polygon or beyond `max_distance`
- are set to NaN.
- """
- # check if it is stop table
- traj_cols_w_deflts = loader._parse_traj_cols(data.columns, traj_cols, kwargs)
- end_col_present = loader._has_end_cols(data.columns, traj_cols_w_deflts)
- duration_col_present = loader._has_duration_cols(data.columns, traj_cols_w_deflts)
- is_stop_table = (end_col_present or duration_col_present)
-
- if is_stop_table:
- # is stop table
- if method=='majority':
- raise TypeError("Method `majority' requires ping data with cluster labels,\
- but a stop table was provided")
- elif method=='centroid':
- stop_table = data.copy()
- location = poi_map(
- data=stop_table,
- poi_table=poi_table,
- max_distance=max_distance,
- data_crs=data_crs,
- location_id=location_id,
- traj_cols=traj_cols,
- **kwargs)
-
- return location
-
- else:
- raise ValueError(f"Method {method} not among implemented methods: `centroid' and `majority'")
-
- else:
- # is labeled pings
- if not cluster_label: #try defaults and raise
- if 'cluster_label' in data.columns:
- cluster_label = 'cluster_label'
- elif 'cluster' in data.columns:
- cluster_label = 'cluster'
- else:
- raise ValueError(f"Argument `cluster_label` is required for visit attribution of labeled pings.")
-
- clustered_pings = data.loc[data[cluster_label] != -1].copy()
- if method=='majority':
- location = poi_map(
- data=clustered_pings,
- poi_table=poi_table,
- max_distance=max_distance,
- data_crs=data_crs,
- location_id=location_id,
- traj_cols=traj_cols,
- **kwargs
- )
- loc_col = location.name
- clustered_pings = clustered_pings.join(location)
-
- location = clustered_pings.groupby(cluster_label)[loc_col].agg(
- lambda x: x.mode().iloc[0] if not x.mode().empty else None)
-
- return data[[cluster_label]].join(location, on=cluster_label)[loc_col]
-
- elif method=='centroid': # should be medoid?
- loader._has_spatial_cols(data.columns, traj_cols, exclusive=True)
- use_lon_lat = ('latitude' in traj_cols and 'longitude' in traj_cols)
- if use_lon_lat:
- warnings.warn("Spherical ('longitude', 'latitude') coordinates were passed. Centroids will not agree with geodetic distances")
- centr_data = clustered_pings.groupby(cluster_label).agg({traj_cols['longitude']:'mean', traj_cols['latitude']:'mean'})
- else:
- centr_data = clustered_pings.groupby(cluster_label).agg({traj_cols['x']:'mean', traj_cols['y']:'mean'})
-
- location = poi_map(
- data=centr_data,
- poi_table=poi_table,
- max_distance=max_distance,
- data_crs=data_crs,
- location_id=location_id,
- traj_cols=traj_cols,
- **kwargs)
- loc_col = location.name
-
- return data[[cluster_label]].join(location, on=cluster_label)[loc_col]
-
- else:
- raise ValueError(f"Method {method} not among implemented methods: `centroid' and `majority'")
-
- return None
-
-# change to point_in_polygon, move to filters.py
-def poi_map(data, poi_table, max_distance=0, data_crs=None, location_id=None, traj_cols=None, **kwargs):
- """
- Assign each point in `data` to a polygon in `poi_table`, using containment when
- `max_distance==0` or the nearest neighbor within `max_distance` otherwise.
-
- Parameters
- ----------
- data : pd.DataFrame or gpd.GeoDataFrame
- Input points, either as a DataFrame with coordinate columns or a GeoDataFrame.
- poi_table : gpd.GeoDataFrame
- Polygons to match against, indexed or with `location_id` column.
- traj_cols : list of str, optional
- Names of the coordinate columns in `data` when it is a DataFrame.
- max_distance : float, default 0
- Maximum search radius for nearest‐neighbor matching; zero invokes a point‐in‐polygon test.
- data_crs : str or pyproj.CRS, optional
- CRS for `data` if it is a DataFrame; ignored for GeoDataFrames.
- location_id : str, optional
- Name of the geometry ID column in `poi_table`; uses the GeoDataFrame index if not provided.
- **kwargs
- Passed to trajectory‐column parsing helper.
-
- Returns
- -------
- pd.Series
- Indexed like `data`, with each entry set to the matching polygon’s ID (from
- `location_id` or `poi_table.index`). Points not contained or beyond `max_distance`
- yield NaN. When multiple polygons overlap a point, only the first match is kept.
- """
- # column name handling
- traj_cols = loader._parse_traj_cols(data.columns, traj_cols, kwargs, defaults={})
-
- if poi_table.crs is None:
- raise ValueError(f"poi_table must have crs attribute for spatial join.")
-
- # Determine which geometry to use
- if isinstance(data, gpd.GeoDataFrame):
- pings_gdf = data.geometry
- # if geodataframe, data_crs is ignored but we Raise if conflicting crs because it is suspect
- if data_crs and not pyproj.CRS(pings_gdf.crs).equals(pyproj.CRS(data_crs)):
- raise ValueError(f"Provided CRS {data_crs} conflicts with traj CRS {data.crs}.")
-
- if isinstance(data, pd.DataFrame):
- # Parse traj_cols with kwargs to get spatial column mappings (using empty defaults to avoid conflicts)
- traj_cols_w_deflts = loader._parse_traj_cols(data.columns, traj_cols, kwargs, defaults={}, warn=False)
- # check that user specified x,y or lat, lon but not both
- loader._has_spatial_cols(data.columns, traj_cols_w_deflts, exclusive=True)
-
- use_lon_lat = ('latitude' in traj_cols_w_deflts and 'longitude' in traj_cols_w_deflts)
-
- if use_lon_lat:
- if data_crs:
- data_crs = pyproj.CRS(data_crs)
- if data_crs.is_projected:
- warnings.warn(f"Provided CRS {data_crs.name} is a projected coordinate system, but "
- "spherical ('longitude', 'latitude') coordinates were passed. Did you mean to pass data_crs='EPSG:4326'?"
- )
- else: # we assume EPSG:4326
- warnings.warn("Argument `data_crs` not provided, assuming EPSG:4326 for ('longitude', 'latitude') coordinates")
- data_crs = pyproj.CRS("EPSG:4326")
-
- pings_gdf= gpd.points_from_xy(
- data[traj_cols_w_deflts['longitude']],
- data[traj_cols_w_deflts['latitude']],
- crs=data_crs) # order matters: lon first
- else:
- if not data_crs:
- raise ValueError(f"data_crs must be provided when using projected coordinates.")
- data_crs = pyproj.CRS(data_crs)
- if data_crs.is_geographic:
- warnings.warn(f"Provided CRS {data_crs.name} is a geographic coordinate system. "
- "This will lead to errors if passed coordinates ('x', 'y') are projected."
- f"Did you mean to use {poi_table.crs}?"
- )
- pings_gdf= gpd.points_from_xy(
- data[traj_cols_w_deflts['x']],
- data[traj_cols_w_deflts['y']],
- crs=data_crs)
- else:
- raise TypeError("`data` must be a pandas DataFrame or a GeoDataFrame.")
-
- if not data_crs.equals(pyproj.CRS(poi_table.crs)):
- poi_table = poi_table.to_crs(data_crs)
- warnings.warn("CRS for `poi_table` does not match crs for `data`. Reprojecting...")
-
- use_poi_idx = True
- if location_id is not None:
- loc_col = location_id
- if location_id in poi_table:
- use_poi_idx=False
- else:
- warnings.warn(f"{location_id} column not found in {poi_table.columns}, defaulting to poi_table.index for spatial join.")
- else:
- loc_col = 'location_id'
- warnings.warn(f"location_id column not provided, defaulting to poi_table.index for spatial join.")
-
-
- if max_distance>0:
- if data_crs.is_geographic:
- warnings.warn(f"Provided CRS {data_crs.name} is a geographic coordinate system. "
- "This will lead to errors when computing euclidean distances."
- f"Did you mean to use `max_distance=0'?"
- )
-
- p_idx, idx = poi_table.sindex.nearest(pings_gdf, max_distance=max_distance, return_all=False)
- if use_poi_idx:
- s = pd.Series(poi_table.iloc[idx].index, index=data.index[p_idx])
- s.name = loc_col
- else:
- s = pd.Series(poi_table.iloc[idx][loc_col].values, index=data.index[p_idx])
- s.name = loc_col
-
- return s.reindex(data.index)
-
- else: # default max_distance = 0
- p_idx, idx = poi_table.sindex.query(pings_gdf, predicate="within") # boundary counts; use "contains" to exclude it
- if use_poi_idx:
- s = pd.Series(poi_table.iloc[idx].index, index=data.index[p_idx]) # might have duplicates
- s = s.loc[~s.index.duplicated()]
- s.name = loc_col
- else:
- s = pd.Series(poi_table.iloc[idx][loc_col].values, index=data.index[p_idx])
- s = s.loc[~s.index.duplicated()]
- s.name = loc_col
- return s.reindex(data.index)
-
-def oracle_map(data, true_visits, traj_cols=None, **kwargs):
- """
- Map elements in traj to ground truth location based solely on time.
-
- Parameters
- ----------
- data : pd.DataFrame
- The trajectory DataFrame containing x and y coordinates.
- true_visits : pd.DataFrame
- A visitation table containing location IDs, start times, and durations/end times.
- traj_cols : list
- The columns in the trajectory DataFrame to be used for mapping.
- **kwargs : dict
- Additional keyword arguments.
-
- Returns
- -------
- pd.Series
- A Series containing the location IDs corresponding to the pings in the trajectory.
- """
- true_visits = true_visits.copy()
- data = data.copy()
-
- # determine temporal columns to use
- t_key_l, use_datetime_l = loader._fallback_time_cols_dt(data.columns, traj_cols, kwargs)
- t_key_r, use_datetime_r = loader._fallback_time_cols_dt(true_visits.columns, traj_cols, kwargs)
-
-
- traj_cols = loader._parse_traj_cols(true_visits.columns, traj_cols, kwargs) #load defaults
- if use_datetime_l != use_datetime_r:
- raise ValueError(f"Mismatch in temporal columns {traj_cols[t_key_l]} vs {traj_cols[t_key_r]}.")
-
- # check is diary table
- end_col_present = loader._has_end_cols(true_visits.columns, traj_cols)
- duration_col_present = loader._has_duration_cols(true_visits.columns, traj_cols)
- if not (end_col_present or duration_col_present):
- raise ValueError("Missing required (end or duration) temporal columns for true_visits dataframe.")
-
- if traj_cols['location_id'] not in true_visits.columns:
- raise ValueError(f"Missing {traj_cols[location_id]} column in {true_visits.columns}."
- "pass `location_id` as keyword argument or in traj_cols."
- )
-
- end_t_key = 'end_datetime' if use_datetime_r else 'end_timestamp'
- if not end_col_present:
- if use_datetime_r:
- true_visits[end_t_key] = true_visits[traj_cols[t_key_r]] + pd.to_timedelta(true_visits[traj_cols['duration']]*60, unit='s')
- else:
- true_visits[end_t_key] = true_visits[traj_cols[t_key_r]] + true_visits[traj_cols['duration']]*60
-
-
- # t_key_l and t_key_r match in type, and end_t_key exists
- data[traj_cols['location_id']] = pd.NA
- for idx, row in true_visits.loc[~true_visits[traj_cols['location_id']].isna()].iterrows():
- start, end, loc = row[traj_cols[t_key_r]], row[traj_cols[end_t_key]], row[traj_cols['location_id']]
- data.loc[(data[traj_cols[t_key_l]]>=start)&(data[traj_cols[t_key_l]]= 0:
- day_parts = [(start.time(), time.max), (time.min, end.time())]
- else:
- full_days = 0
- day_parts = [(start.time(), end.time()), (start.time(), start.time())]
- return full_days, day_parts
-
-def duration_at_night_fast(start, end, dawn_hour = 6, dusk_hour = 19):
- full_days, (part1, part2) = slice_datetimes_interval_fast(start, end)
- total_dawn_time = dawn_time(part1, dawn_hour)+dawn_time(part2, dawn_hour)
- total_dusk_time = dusk_time(part1, dusk_hour)+dusk_time(part2, dusk_hour)
- return int(total_dawn_time + total_dusk_time + full_days*(dawn_hour + (24-dusk_hour))*60)
-
-def clip_stays_date(traj, dates, dawn_hour = 6, dusk_hour = 19):
- start = pd.to_datetime(traj['start_datetime'])
- duration = traj['duration']
-
- # Ensure timezone-aware clipping bounds
- tz = start.dt.tz
- date_0 = pd.Timestamp(parse(dates[0]), tz=tz)
- date_1 = pd.Timestamp(parse(dates[1]), tz=tz)
-
- end = start + pd.to_timedelta(duration, unit='m')
-
- # Clip to date range
- start_clipped = start.clip(lower=date_0, upper=date_1)
- end_clipped = end.clip(lower=date_0, upper=date_1)
-
- # Recompute durations
- duration_clipped = ((end_clipped - start_clipped).dt.total_seconds() // 60).astype(int)
- duration_night = [duration_at_night_fast(s, e, dawn_hour, dusk_hour) for s, e in zip(start_clipped, end_clipped)]
-
- return pd.DataFrame({
- 'id': traj['id'].values,
- 'start': start_clipped,
- 'duration': duration_clipped,
- 'duration_night': duration_night,
- 'location': traj['location']
- })
-
-def count_nights(usr_polygon, dawn_hour = 6, dusk_hour = 19, min_dwell = 10):
- nights = set()
- weeks = set()
-
- for _, row in usr_polygon.iterrows():
- d = row['start']
- d = pd.to_datetime(d)
- full_days, (part1, part2) = slice_datetimes_interval_fast(d, d + pd.to_timedelta(row['duration'], unit='m'))
-
- dawn1 = dawn_time(part1, dawn_hour)
- dusk1 = dusk_time(part1, dusk_hour)
- dawn2 = dawn_time(part2, dawn_hour)
- dusk2 = dusk_time(part2, dusk_hour)
-
- if full_days == 0:
- if dawn1 >= min_dwell:
- night = d - timedelta(days=1)
- nights.add(night.date())
- weeks.add((night - timedelta(days=night.weekday())).date())
-
- if (dusk1 + dawn2) >= min_dwell:
- night = d
- nights.add(night.date())
- weeks.add((night - timedelta(days=night.weekday())).date())
-
- if dusk2 >= min_dwell:
- night = d + timedelta(days=1)
- nights.add(night.date())
- weeks.add((night - timedelta(days=night.weekday())).date())
- else:
- if dawn1 >= min_dwell:
- night = d - timedelta(days=1)
- nights.add(night.date())
- weeks.add((night - timedelta(days=night.weekday())).date())
-
- for t in range(full_days + 1):
- night = d + timedelta(days=t)
- nights.add(night.date())
- weeks.add((night - timedelta(days=night.weekday())).date())
-
- if dusk2 >= min_dwell:
- night = d + timedelta(days=full_days + 1)
- nights.add(night.date())
- weeks.add((night - timedelta(days=night.weekday())).date())
-
- identifier = usr_polygon['id'].iloc[0]
- location = usr_polygon['location'].iloc[0]
-
- return pd.DataFrame([{
- 'id': identifier,
- 'location': location,
- 'night_count': len(nights),
- 'week_count': len(weeks)
- }])
-
-
-def night_stops(stop_table, user='user', dawn_hour = 6, dusk_hour = 19, min_dwell = 10):
- # Date range
- start_date = str(stop_table['start_datetime'].min().date())
- weeks = stop_table['start_datetime'].dt.strftime('%Y-%U')
- num_weeks = weeks.nunique()
-
- # turn dates to datetime
- stop_table['start_datetime'] = pd.to_datetime(stop_table['start_datetime'])
-
- if 'id' not in stop_table.columns:
- stop_table['id'] = user
-
- end_date = (parse(start_date) + timedelta(weeks=num_weeks)).date().isoformat()
- dates = (start_date, end_date)
- df_clipped = clip_stays_date(stop_table, dates, dawn_hour, dusk_hour)
- df_clipped = df_clipped[(df_clipped['duration'] > 0) & (df_clipped['duration_night'] >= 15)]
-
- return df_clipped.groupby(['id', 'location'], group_keys=False).apply(count_nights(dawn_hour, dusk_hour, min_dwell)).reset_index(drop=True)
-
+import geopandas as gpd
+import nomad.io.base as loader
+import nomad.constants as constants
+import warnings
+import pandas as pd
+import nomad.io.base as loader
+import pyproj
+import pdb
+
+# TO DO: change to stops_to_poi
+def point_in_polygon(data, poi_table, method='centroid', data_crs=None, max_distance=0,
+ cluster_label=None, location_id=None, traj_cols=None, **kwargs):
+ """
+ Assign each stop or cluster of pings in `data` to a polygon in `poi_table`,
+ either by the cluster’s centroid location or by the most frequent polygon hit.
+
+ Parameters
+ ----------
+ data : pd.DataFrame or gpd.GeoDataFrame
+ A table of pings (with optional stop/duration columns) or stops,
+ indexed by observation or cluster.
+ poi_table : gpd.GeoDataFrame
+ Polygons to match against, with CRS set and optional ID column.
+ method : {'centroid', 'majority'}, default 'centroid'
+ ‘centroid’ uses each cluster’s mean point; ‘majority’ picks the polygon
+ most often visited within each cluster (only for ping data).
+ data_crs : str or pyproj.CRS, optional
+ CRS for `data` when it is a plain DataFrame; ignored if `data` is a GeoDataFrame.
+ max_distance : float, default 0
+ Search radius for nearest‐neighbor fall-back; zero triggers strict
+ point-in-polygon matching.
+ cluster_label : str, optional
+ Column name holding cluster IDs in ping data; inferred from `data` if absent.
+ location_id : str, optional
+ Column in `poi_table` containing the output ID; uses the GeoDataFrame index if None.
+ traj_cols : list of str, optional
+ Names of the coordinate columns in `data` when it is a DataFrame.
+ **kwargs
+ Passed through to `poi_map` or the trajectory-column parser.
+
+ Returns
+ -------
+ pd.Series
+ Indexed like `data`, giving the matched polygon ID for each stop or ping.
+ Points or clusters that fall outside every polygon or beyond `max_distance`
+ are set to NaN.
+ """
+ # check if it is stop table
+ traj_cols_w_deflts = loader._parse_traj_cols(data.columns, traj_cols, kwargs)
+ end_col_present = loader._has_end_cols(data.columns, traj_cols_w_deflts)
+ duration_col_present = loader._has_duration_cols(data.columns, traj_cols_w_deflts)
+ is_stop_table = (end_col_present or duration_col_present)
+
+ if is_stop_table:
+ # is stop table
+ if method=='majority':
+ raise TypeError("Method `majority' requires ping data with cluster labels,\
+ but a stop table was provided")
+ elif method=='centroid':
+ stop_table = data.copy()
+ location = poi_map(
+ data=stop_table,
+ poi_table=poi_table,
+ max_distance=max_distance,
+ data_crs=data_crs,
+ location_id=location_id,
+ traj_cols=traj_cols,
+ **kwargs)
+
+ return location
+
+ else:
+ raise ValueError(f"Method {method} not among implemented methods: `centroid' and `majority'")
+
+ else:
+ # is labeled pings
+ if not cluster_label: #try defaults and raise
+ if 'cluster_label' in data.columns:
+ cluster_label = 'cluster_label'
+ elif 'cluster' in data.columns:
+ cluster_label = 'cluster'
+ else:
+ raise ValueError(f"Argument `cluster_label` is required for visit attribution of labeled pings.")
+
+ clustered_pings = data.loc[data[cluster_label] != -1].copy()
+ if method=='majority':
+ location = poi_map(
+ data=clustered_pings,
+ poi_table=poi_table,
+ max_distance=max_distance,
+ data_crs=data_crs,
+ location_id=location_id,
+ traj_cols=traj_cols,
+ **kwargs
+ )
+ loc_col = location.name
+ clustered_pings = clustered_pings.join(location)
+
+ location = clustered_pings.groupby(cluster_label)[loc_col].agg(
+ lambda x: x.mode().iloc[0] if not x.mode().empty else None)
+
+ return data[[cluster_label]].join(location, on=cluster_label)[loc_col]
+
+ elif method=='centroid': # should be medoid?
+ loader._has_spatial_cols(data.columns, traj_cols, exclusive=True)
+ use_lon_lat = ('latitude' in traj_cols and 'longitude' in traj_cols)
+ if use_lon_lat:
+ warnings.warn("Spherical ('longitude', 'latitude') coordinates were passed. Centroids will not agree with geodetic distances")
+ centr_data = clustered_pings.groupby(cluster_label).agg({traj_cols['longitude']:'mean', traj_cols['latitude']:'mean'})
+ else:
+ centr_data = clustered_pings.groupby(cluster_label).agg({traj_cols['x']:'mean', traj_cols['y']:'mean'})
+
+ location = poi_map(
+ data=centr_data,
+ poi_table=poi_table,
+ max_distance=max_distance,
+ data_crs=data_crs,
+ location_id=location_id,
+ traj_cols=traj_cols,
+ **kwargs)
+ loc_col = location.name
+
+ return data[[cluster_label]].join(location, on=cluster_label)[loc_col]
+
+ else:
+ raise ValueError(f"Method {method} not among implemented methods: `centroid' and `majority'")
+
+ return None
+
+# change to point_in_polygon, move to filters.py
+def poi_map(data, poi_table, max_distance=0, data_crs=None, location_id=None, traj_cols=None, **kwargs):
+ """
+ Assign each point in `data` to a polygon in `poi_table`, using containment when
+ `max_distance==0` or the nearest neighbor within `max_distance` otherwise.
+
+ Parameters
+ ----------
+ data : pd.DataFrame or gpd.GeoDataFrame
+ Input points, either as a DataFrame with coordinate columns or a GeoDataFrame.
+ poi_table : gpd.GeoDataFrame
+ Polygons to match against, indexed or with `location_id` column.
+ traj_cols : list of str, optional
+ Names of the coordinate columns in `data` when it is a DataFrame.
+ max_distance : float, default 0
+ Maximum search radius for nearest‐neighbor matching; zero invokes a point‐in‐polygon test.
+ data_crs : str or pyproj.CRS, optional
+ CRS for `data` if it is a DataFrame; ignored for GeoDataFrames.
+ location_id : str, optional
+ Name of the geometry ID column in `poi_table`; uses the GeoDataFrame index if not provided.
+ **kwargs
+ Passed to trajectory‐column parsing helper.
+
+ Returns
+ -------
+ pd.Series
+ Indexed like `data`, with each entry set to the matching polygon’s ID (from
+ `location_id` or `poi_table.index`). Points not contained or beyond `max_distance`
+ yield NaN. When multiple polygons overlap a point, only the first match is kept.
+ """
+ # column name handling
+ traj_cols = loader._parse_traj_cols(data.columns, traj_cols, kwargs, defaults={})
+
+ if poi_table.crs is None:
+ raise ValueError(f"poi_table must have crs attribute for spatial join.")
+
+ # Determine which geometry to use
+ if isinstance(data, gpd.GeoDataFrame):
+ pings_gdf = data.geometry
+ # if geodataframe, data_crs is ignored but we Raise if conflicting crs because it is suspect
+ if data_crs and not pyproj.CRS(pings_gdf.crs).equals(pyproj.CRS(data_crs)):
+ raise ValueError(f"Provided CRS {data_crs} conflicts with traj CRS {data.crs}.")
+
+ if isinstance(data, pd.DataFrame):
+ # Parse traj_cols with kwargs to get spatial column mappings (using empty defaults to avoid conflicts)
+ traj_cols_w_deflts = loader._parse_traj_cols(data.columns, traj_cols, kwargs, defaults={}, warn=False)
+ # check that user specified x,y or lat, lon but not both
+ loader._has_spatial_cols(data.columns, traj_cols_w_deflts, exclusive=True)
+
+ use_lon_lat = ('latitude' in traj_cols_w_deflts and 'longitude' in traj_cols_w_deflts)
+
+ if use_lon_lat:
+ if data_crs:
+ data_crs = pyproj.CRS(data_crs)
+ if data_crs.is_projected:
+ warnings.warn(f"Provided CRS {data_crs.name} is a projected coordinate system, but "
+ "spherical ('longitude', 'latitude') coordinates were passed. Did you mean to pass data_crs='EPSG:4326'?"
+ )
+ else: # we assume EPSG:4326
+ warnings.warn("Argument `data_crs` not provided, assuming EPSG:4326 for ('longitude', 'latitude') coordinates")
+ data_crs = pyproj.CRS("EPSG:4326")
+
+ pings_gdf= gpd.points_from_xy(
+ data[traj_cols_w_deflts['longitude']],
+ data[traj_cols_w_deflts['latitude']],
+ crs=data_crs) # order matters: lon first
+ else:
+ if not data_crs:
+ raise ValueError(f"data_crs must be provided when using projected coordinates.")
+ data_crs = pyproj.CRS(data_crs)
+ if data_crs.is_geographic:
+ warnings.warn(f"Provided CRS {data_crs.name} is a geographic coordinate system. "
+ "This will lead to errors if passed coordinates ('x', 'y') are projected."
+ f"Did you mean to use {poi_table.crs}?"
+ )
+ pings_gdf= gpd.points_from_xy(
+ data[traj_cols_w_deflts['x']],
+ data[traj_cols_w_deflts['y']],
+ crs=data_crs)
+ else:
+ raise TypeError("`data` must be a pandas DataFrame or a GeoDataFrame.")
+
+ if not data_crs.equals(pyproj.CRS(poi_table.crs)):
+ poi_table = poi_table.to_crs(data_crs)
+ warnings.warn("CRS for `poi_table` does not match crs for `data`. Reprojecting...")
+
+ use_poi_idx = True
+ if location_id is not None:
+ loc_col = location_id
+ if location_id in poi_table:
+ use_poi_idx=False
+ else:
+ warnings.warn(f"{location_id} column not found in {poi_table.columns}, defaulting to poi_table.index for spatial join.")
+ else:
+ loc_col = 'location_id'
+ warnings.warn(f"location_id column not provided, defaulting to poi_table.index for spatial join.")
+
+
+ if max_distance>0:
+ if data_crs.is_geographic:
+ warnings.warn(f"Provided CRS {data_crs.name} is a geographic coordinate system. "
+ "This will lead to errors when computing euclidean distances."
+ f"Did you mean to use `max_distance=0'?"
+ )
+
+ p_idx, idx = poi_table.sindex.nearest(pings_gdf, max_distance=max_distance, return_all=False)
+ if use_poi_idx:
+ s = pd.Series(poi_table.iloc[idx].index, index=data.index[p_idx])
+ s.name = loc_col
+ else:
+ s = pd.Series(poi_table.iloc[idx][loc_col].values, index=data.index[p_idx])
+ s.name = loc_col
+
+ return s.reindex(data.index)
+
+ else: # default max_distance = 0
+ p_idx, idx = poi_table.sindex.query(pings_gdf, predicate="within") # boundary counts; use "contains" to exclude it
+ if use_poi_idx:
+ s = pd.Series(poi_table.iloc[idx].index, index=data.index[p_idx]) # might have duplicates
+ s = s.loc[~s.index.duplicated()]
+ s.name = loc_col
+ else:
+ s = pd.Series(poi_table.iloc[idx][loc_col].values, index=data.index[p_idx])
+ s = s.loc[~s.index.duplicated()]
+ s.name = loc_col
+ return s.reindex(data.index)
+
+def oracle_map(data, true_visits, traj_cols=None, **kwargs):
+ """
+ Map elements in traj to ground truth location based solely on time.
+
+ Parameters
+ ----------
+ data : pd.DataFrame
+ The trajectory DataFrame containing x and y coordinates.
+ true_visits : pd.DataFrame
+ A visitation table containing location IDs, start times, and durations/end times.
+ traj_cols : list
+ The columns in the trajectory DataFrame to be used for mapping.
+ **kwargs : dict
+ Additional keyword arguments.
+
+ Returns
+ -------
+ pd.Series
+ A Series containing the location IDs corresponding to the pings in the trajectory.
+ """
+ true_visits = true_visits.copy()
+ data = data.copy()
+
+ # determine temporal columns to use
+ t_key_l, use_datetime_l = loader._fallback_time_cols_dt(data.columns, traj_cols, kwargs)
+ t_key_r, use_datetime_r = loader._fallback_time_cols_dt(true_visits.columns, traj_cols, kwargs)
+
+
+ traj_cols = loader._parse_traj_cols(true_visits.columns, traj_cols, kwargs) #load defaults
+ if use_datetime_l != use_datetime_r:
+ raise ValueError(f"Mismatch in temporal columns {traj_cols[t_key_l]} vs {traj_cols[t_key_r]}.")
+
+ # check is diary table
+ end_col_present = loader._has_end_cols(true_visits.columns, traj_cols)
+ duration_col_present = loader._has_duration_cols(true_visits.columns, traj_cols)
+ if not (end_col_present or duration_col_present):
+ raise ValueError("Missing required (end or duration) temporal columns for true_visits dataframe.")
+
+ if traj_cols['location_id'] not in true_visits.columns:
+ raise ValueError(f"Missing {traj_cols[location_id]} column in {true_visits.columns}."
+ "pass `location_id` as keyword argument or in traj_cols."
+ )
+
+ end_t_key = 'end_datetime' if use_datetime_r else 'end_timestamp'
+ if not end_col_present:
+ if use_datetime_r:
+ true_visits[end_t_key] = true_visits[traj_cols[t_key_r]] + pd.to_timedelta(true_visits[traj_cols['duration']]*60, unit='s')
+ else:
+ true_visits[end_t_key] = true_visits[traj_cols[t_key_r]] + true_visits[traj_cols['duration']]*60
+
+
+ # t_key_l and t_key_r match in type, and end_t_key exists
+ data[traj_cols['location_id']] = pd.NA
+ for idx, row in true_visits.loc[~true_visits[traj_cols['location_id']].isna()].iterrows():
+ start, end, loc = row[traj_cols[t_key_r]], row[traj_cols[end_t_key]], row[traj_cols['location_id']]
+ data.loc[(data[traj_cols[t_key_l]]>=start)&(data[traj_cols[t_key_l]]= 0:
+ day_parts = [(start.time(), time.max), (time.min, end.time())]
+ else:
+ full_days = 0
+ day_parts = [(start.time(), end.time()), (start.time(), start.time())]
+ return full_days, day_parts
+
+def duration_at_night_fast(start, end, dawn_hour = 6, dusk_hour = 19):
+ full_days, (part1, part2) = slice_datetimes_interval_fast(start, end)
+ total_dawn_time = dawn_time(part1, dawn_hour)+dawn_time(part2, dawn_hour)
+ total_dusk_time = dusk_time(part1, dusk_hour)+dusk_time(part2, dusk_hour)
+ return int(total_dawn_time + total_dusk_time + full_days*(dawn_hour + (24-dusk_hour))*60)
+
+def clip_stays_date(traj, dates, dawn_hour = 6, dusk_hour = 19):
+ start = pd.to_datetime(traj['start_datetime'])
+ duration = traj['duration']
+
+ # Ensure timezone-aware clipping bounds
+ tz = start.dt.tz
+ date_0 = pd.Timestamp(parse(dates[0]), tz=tz)
+ date_1 = pd.Timestamp(parse(dates[1]), tz=tz)
+
+ end = start + pd.to_timedelta(duration, unit='m')
+
+ # Clip to date range
+ start_clipped = start.clip(lower=date_0, upper=date_1)
+ end_clipped = end.clip(lower=date_0, upper=date_1)
+
+ # Recompute durations
+ duration_clipped = ((end_clipped - start_clipped).dt.total_seconds() // 60).astype(int)
+ duration_night = [duration_at_night_fast(s, e, dawn_hour, dusk_hour) for s, e in zip(start_clipped, end_clipped)]
+
+ return pd.DataFrame({
+ 'id': traj['id'].values,
+ 'start': start_clipped,
+ 'duration': duration_clipped,
+ 'duration_night': duration_night,
+ 'location': traj['location']
+ })
+
+def count_nights(usr_polygon, dawn_hour = 6, dusk_hour = 19, min_dwell = 10):
+ nights = set()
+ weeks = set()
+
+ for _, row in usr_polygon.iterrows():
+ d = row['start']
+ d = pd.to_datetime(d)
+ full_days, (part1, part2) = slice_datetimes_interval_fast(d, d + pd.to_timedelta(row['duration'], unit='m'))
+
+ dawn1 = dawn_time(part1, dawn_hour)
+ dusk1 = dusk_time(part1, dusk_hour)
+ dawn2 = dawn_time(part2, dawn_hour)
+ dusk2 = dusk_time(part2, dusk_hour)
+
+ if full_days == 0:
+ if dawn1 >= min_dwell:
+ night = d - timedelta(days=1)
+ nights.add(night.date())
+ weeks.add((night - timedelta(days=night.weekday())).date())
+
+ if (dusk1 + dawn2) >= min_dwell:
+ night = d
+ nights.add(night.date())
+ weeks.add((night - timedelta(days=night.weekday())).date())
+
+ if dusk2 >= min_dwell:
+ night = d + timedelta(days=1)
+ nights.add(night.date())
+ weeks.add((night - timedelta(days=night.weekday())).date())
+ else:
+ if dawn1 >= min_dwell:
+ night = d - timedelta(days=1)
+ nights.add(night.date())
+ weeks.add((night - timedelta(days=night.weekday())).date())
+
+ for t in range(full_days + 1):
+ night = d + timedelta(days=t)
+ nights.add(night.date())
+ weeks.add((night - timedelta(days=night.weekday())).date())
+
+ if dusk2 >= min_dwell:
+ night = d + timedelta(days=full_days + 1)
+ nights.add(night.date())
+ weeks.add((night - timedelta(days=night.weekday())).date())
+
+ identifier = usr_polygon['id'].iloc[0]
+ location = usr_polygon['location'].iloc[0]
+
+ return pd.DataFrame([{
+ 'id': identifier,
+ 'location': location,
+ 'night_count': len(nights),
+ 'week_count': len(weeks)
+ }])
+
+
+def night_stops(stop_table, user='user', dawn_hour = 6, dusk_hour = 19, min_dwell = 10):
+ # Date range
+ start_date = str(stop_table['start_datetime'].min().date())
+ weeks = stop_table['start_datetime'].dt.strftime('%Y-%U')
+ num_weeks = weeks.nunique()
+
+ # turn dates to datetime
+ stop_table['start_datetime'] = pd.to_datetime(stop_table['start_datetime'])
+
+ if 'id' not in stop_table.columns:
+ stop_table['id'] = user
+
+ end_date = (parse(start_date) + timedelta(weeks=num_weeks)).date().isoformat()
+ dates = (start_date, end_date)
+ df_clipped = clip_stays_date(stop_table, dates, dawn_hour, dusk_hour)
+ df_clipped = df_clipped[(df_clipped['duration'] > 0) & (df_clipped['duration_night'] >= 15)]
+
+ return df_clipped.groupby(['id', 'location'], group_keys=False).apply(count_nights(dawn_hour, dusk_hour, min_dwell)).reset_index(drop=True)
+