deepSSF 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deepssf-0.3.0 → deepssf-0.3.1}/CHANGELOG.md +45 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/CITATION.cff +1 -1
- {deepssf-0.3.0 → deepssf-0.3.1}/PKG-INFO +1 -1
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_v0_3_0.ipynb +457 -28
- deepssf-0.3.1/examples/deepssf_train_validate_example_v0_3_1.ipynb +2982 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/__init__.py +1 -1
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/data.py +34 -7
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/simulate.py +65 -10
- {deepssf-0.3.0 → deepssf-0.3.1}/.github/workflows/ci.yml +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/.github/workflows/publish.yml +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/.gitignore +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/.pre-commit-config.yaml +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/Habitat_movement_loss_balancing.md +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/LICENSE +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/Package_generation_process.md +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/README.md +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/environment.yml +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example.html +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/figure-html/cell-25-output-1.png +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/figure-html/cell-29-output-1.png +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/figure-html/cell-32-output-1.png +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/figure-html/cell-7-output-1.png +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/bootstrap/bootstrap-138a6193a3bd40baf1e627da441a4734.min.css +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/bootstrap/bootstrap-icons.css +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/bootstrap/bootstrap-icons.woff +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/bootstrap/bootstrap.min.js +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/clipboard/clipboard.min.js +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/quarto-html/anchor.min.js +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/quarto-html/popper.min.js +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/quarto-html/quarto-syntax-highlighting-7f8f88aac4f3542376d5c11b86a4c14d.css +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/quarto-html/quarto.js +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/quarto-html/tabsets/tabsets.js +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/quarto-html/tippy.css +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/deepssf_train_validate_example_files/libs/quarto-html/tippy.umd.min.js +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/examples/references.bib +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/icons/both_icons.png +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/icons/deepSSF_hex_textured_Python.png +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/icons/deepSSF_hex_textured_R.png +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/pyproject.toml +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/datasets/data/buffalo_djelk_id2005.csv +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/datasets/data/ndvi_2005.tif +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/datasets/data/slope_2005.tif +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/model.py +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/plot.py +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/predict.py +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/train.py +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/utils.py +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/src/deepssf/validate.py +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/styles.css +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/tests/__init__.py +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/tests/test_deepssf.py +0 -0
- {deepssf-0.3.0 → deepssf-0.3.1}/tests/test_smoke.py +0 -0
|
@@ -4,6 +4,51 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.3.1] - 2026-10-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Step length and turning angle columns** — `prepare_movement_df` now returns
|
|
11
|
+
`sl_` (step length, the Euclidean length of `(dx, dy)`, placed after `dx`/`dy`)
|
|
12
|
+
and `ta_` (turning angle, placed after `bearing_tm1`). `ta_` is
|
|
13
|
+
`bearing - bearing_tm1` wrapped to [-π, π), so a slight turn across the ±π
|
|
14
|
+
boundary is not reported as a near-full rotation; positive values are
|
|
15
|
+
anticlockwise turns, following the `arctan2` bearing convention. It is NaN for
|
|
16
|
+
each individual's first step, which has no previous bearing. Neither column is
|
|
17
|
+
a model input.
|
|
18
|
+
- **`tz` argument to `prepare_movement_df`** — the timezone to express
|
|
19
|
+
timestamps and time-derived columns in, e.g. `"Australia/Darwin"`. When
|
|
20
|
+
omitted, the timezone of a tz-aware time column is kept; tz-naive times are
|
|
21
|
+
still treated as UTC.
|
|
22
|
+
- **Timestamps on simulated trajectories** — `simulate_trajectory`,
|
|
23
|
+
`simulate_trajectories_batched` and `simulate_trajectories` take an optional
|
|
24
|
+
`start_time` and, when it is given, add a `time` column (before `hour`) rolled
|
|
25
|
+
forward from it by `time_between_steps`. Rolling forward rather than rebuilding
|
|
26
|
+
from `yday`/`hour` keeps the year (which `yday` loses when it wraps at 365)
|
|
27
|
+
and the timezone of `start_time`. It only labels the output — the model still
|
|
28
|
+
reads `starting_yday`/`starting_hour`, so these should describe the same
|
|
29
|
+
moment. `simulate_trajectories` accepts one start time per trajectory under
|
|
30
|
+
`method="sequential"`/`"parallel"`; `"batched"` requires a single value, as it
|
|
31
|
+
does for the other start-time arguments.
|
|
32
|
+
- **Example notebook `deepssf_train_validate_example_v0_3_1.ipynb`** — adds
|
|
33
|
+
diagnostic summaries of the observed and simulated data: step length and
|
|
34
|
+
turning angle histograms, and both by hour of day. Turning angles are
|
|
35
|
+
summarised per hour by the circular SD and the shortest arc containing 50% of
|
|
36
|
+
the angles (which may wrap across ±π), since a mean or median sits near zero
|
|
37
|
+
whatever the spread.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- **Times were converted to UTC in `prepare_movement_df`.** Timestamps were
|
|
41
|
+
parsed with `utc=True` and the input's timezone was never restored, so a
|
|
42
|
+
tz-aware time column (e.g. converted to `Australia/Darwin`) still produced
|
|
43
|
+
`hour_t1`, `yday_t1` and their sine/cosine encodings in UTC — 9.5 h out for
|
|
44
|
+
Darwin, misplacing diel patterns such as dawn and dusk activity peaks. These
|
|
45
|
+
columns are now in local time, and `t1_`/`t2_` are tz-aware timestamps
|
|
46
|
+
rather than the raw input values. The Sentinel-2 month look-up in
|
|
47
|
+
`MovementDataset`, which reads `t1_`, now uses the local month too.
|
|
48
|
+
**Models trained with ≤ 0.3.0 on tz-aware times learned UTC hour encodings**,
|
|
49
|
+
so retrain them (and re-run any simulations) before comparing against data
|
|
50
|
+
prepared with this version.
|
|
51
|
+
|
|
7
52
|
## [0.3.0] - 2026-10-23
|
|
8
53
|
|
|
9
54
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: deepSSF
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Deep learning step selection functions for predicting animal movement.
|
|
5
5
|
Project-URL: Homepage, https://swforrest.github.io/deepSSF/
|
|
6
6
|
Project-URL: Repository, https://github.com/swforrest/deepssf
|
|
@@ -143,11 +143,11 @@
|
|
|
143
143
|
"# Sort by the individual ID and timestamp to ensure correct temporal order\n",
|
|
144
144
|
"raw_df = raw_df.sort_values(by=[\"id\", \"time\"]).reset_index(drop=True)\n",
|
|
145
145
|
"\n",
|
|
146
|
-
"# # Convert the 'time' column to the correct timezone (here Australia/Darwin) \n",
|
|
147
|
-
"raw_df[\"time\"] = (\n",
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
146
|
+
"# # # Convert the 'time' column to the correct timezone (here Australia/Darwin) \n",
|
|
147
|
+
"# raw_df[\"time\"] = (\n",
|
|
148
|
+
"# pd.to_datetime(raw_df[\"time\"], utc=True)\n",
|
|
149
|
+
"# .dt.tz_convert(\"Australia/Darwin\")\n",
|
|
150
|
+
"# )\n",
|
|
151
151
|
"\n",
|
|
152
152
|
"raw_df.head()"
|
|
153
153
|
]
|
|
@@ -253,7 +253,8 @@
|
|
|
253
253
|
" id_col=\"id\", \n",
|
|
254
254
|
" time_col=\"time\", \n",
|
|
255
255
|
" x_col=\"x\", \n",
|
|
256
|
-
" y_col=\"y\"
|
|
256
|
+
" y_col=\"y\",\n",
|
|
257
|
+
" tz=\"Australia/Darwin\"\n",
|
|
257
258
|
")\n",
|
|
258
259
|
"\n",
|
|
259
260
|
"n_dropped = len(raw_df) - len(step_df)\n",
|
|
@@ -261,6 +262,195 @@
|
|
|
261
262
|
"step_df.head()"
|
|
262
263
|
]
|
|
263
264
|
},
|
|
265
|
+
{
|
|
266
|
+
"cell_type": "markdown",
|
|
267
|
+
"metadata": {},
|
|
268
|
+
"source": [
|
|
269
|
+
"### Check the timezone"
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"cell_type": "code",
|
|
274
|
+
"execution_count": null,
|
|
275
|
+
"metadata": {},
|
|
276
|
+
"outputs": [],
|
|
277
|
+
"source": [
|
|
278
|
+
"step_df[\"t1_\"].dt.tz"
|
|
279
|
+
]
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"cell_type": "markdown",
|
|
283
|
+
"metadata": {},
|
|
284
|
+
"source": [
|
|
285
|
+
"## Visualise some data summaries\n",
|
|
286
|
+
"\n",
|
|
287
|
+
"Partly this is diagnostic, to check whether the time is in the correct timezone (we know that buffalo have high movement periods around dawn and dusk, so we can see whether that aligns). We can also check these summaries against summaries of the simulated trajectories to see whether we've captured these behaviours.\n",
|
|
288
|
+
"\n",
|
|
289
|
+
"### Distribution of step lengths"
|
|
290
|
+
]
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"cell_type": "code",
|
|
294
|
+
"execution_count": null,
|
|
295
|
+
"metadata": {},
|
|
296
|
+
"outputs": [],
|
|
297
|
+
"source": [
|
|
298
|
+
"# Distribution of step lengths, with the median and mean marked\n",
|
|
299
|
+
"fig, ax = plt.subplots(figsize=(8, 4))\n",
|
|
300
|
+
"ax.hist(step_df[\"sl_\"], bins=250, color=\"tab:blue\", alpha=0.7)\n",
|
|
301
|
+
"ax.axvline(step_df[\"sl_\"].median(), color=\"black\", lw=2, label=\"Median\")\n",
|
|
302
|
+
"ax.axvline(step_df[\"sl_\"].mean(), color=\"black\", lw=2, ls=\"--\", label=\"Mean\")\n",
|
|
303
|
+
"ax.set_xlabel(\"Step length (m)\")\n",
|
|
304
|
+
"ax.set_ylabel(\"Count\")\n",
|
|
305
|
+
"ax.set_xlim(0, 2000)\n",
|
|
306
|
+
"ax.legend()\n",
|
|
307
|
+
"plt.tight_layout()\n",
|
|
308
|
+
"plt.show()"
|
|
309
|
+
]
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"cell_type": "markdown",
|
|
313
|
+
"metadata": {},
|
|
314
|
+
"source": [
|
|
315
|
+
"### Log of step lengths"
|
|
316
|
+
]
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"cell_type": "code",
|
|
320
|
+
"execution_count": null,
|
|
321
|
+
"metadata": {},
|
|
322
|
+
"outputs": [],
|
|
323
|
+
"source": [
|
|
324
|
+
"# Distribution of log step lengths, with the median and mean marked\n",
|
|
325
|
+
"fig, ax = plt.subplots(figsize=(8, 4))\n",
|
|
326
|
+
"ax.hist(np.log(step_df[\"sl_\"]), bins=250, color=\"tab:blue\", alpha=0.7)\n",
|
|
327
|
+
"ax.axvline(np.log(step_df[\"sl_\"].median()), color=\"black\", lw=2, label=\"Median\")\n",
|
|
328
|
+
"ax.axvline(np.log(step_df[\"sl_\"].mean()), color=\"black\", lw=2, ls=\"--\", label=\"Mean\")\n",
|
|
329
|
+
"ax.set_xlabel(\"Log step length (m)\")\n",
|
|
330
|
+
"ax.set_ylabel(\"Count\")\n",
|
|
331
|
+
"ax.legend()\n",
|
|
332
|
+
"plt.tight_layout()\n",
|
|
333
|
+
"plt.show()"
|
|
334
|
+
]
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"cell_type": "markdown",
|
|
338
|
+
"metadata": {},
|
|
339
|
+
"source": [
|
|
340
|
+
"### Step lengths through time"
|
|
341
|
+
]
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"cell_type": "code",
|
|
345
|
+
"execution_count": null,
|
|
346
|
+
"metadata": {},
|
|
347
|
+
"outputs": [],
|
|
348
|
+
"source": [
|
|
349
|
+
"# Step length by hour of day, with the hourly median and mean overlaid\n",
|
|
350
|
+
"hourly_sl_median = step_df.groupby(step_df[\"hour_t1\"].round().mod(24))[\"sl_\"].median()\n",
|
|
351
|
+
"hourly_sl_mean = step_df.groupby(step_df[\"hour_t1\"].round().mod(24))[\"sl_\"].mean()\n",
|
|
352
|
+
"\n",
|
|
353
|
+
"fig, ax = plt.subplots(figsize=(8, 4))\n",
|
|
354
|
+
"ax.scatter(step_df[\"hour_t1\"], step_df[\"sl_\"], s=4, alpha=0.15, color=\"tab:blue\", label=\"Steps\")\n",
|
|
355
|
+
"ax.plot(hourly_sl_mean.index, hourly_sl_mean.values, color=\"red\", lw=2, marker=\"s\", ms=4, label=\"Hourly mean\")\n",
|
|
356
|
+
"ax.plot(hourly_sl_median.index, hourly_sl_median.values, color=\"black\", lw=2, marker=\"o\", ms=4, label=\"Hourly median\")\n",
|
|
357
|
+
"ax.set_xlabel(\"Hour of day\")\n",
|
|
358
|
+
"ax.set_ylabel(\"Step length (m)\")\n",
|
|
359
|
+
"ax.set_xticks(range(0, 25, 3))\n",
|
|
360
|
+
"ax.set_xlim(0, 24)\n",
|
|
361
|
+
"ax.set_ylim(0, 1000)\n",
|
|
362
|
+
"ax.legend()\n",
|
|
363
|
+
"plt.tight_layout()\n",
|
|
364
|
+
"plt.show()"
|
|
365
|
+
]
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"cell_type": "markdown",
|
|
369
|
+
"metadata": {},
|
|
370
|
+
"source": [
|
|
371
|
+
"### Distribution of turning angles"
|
|
372
|
+
]
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"cell_type": "code",
|
|
376
|
+
"execution_count": null,
|
|
377
|
+
"metadata": {},
|
|
378
|
+
"outputs": [],
|
|
379
|
+
"source": [
|
|
380
|
+
"# Distribution of turning angles (NaN for each individual's first step, so dropped)\n",
|
|
381
|
+
"fig, ax = plt.subplots(figsize=(8, 4))\n",
|
|
382
|
+
"ax.hist(step_df[\"ta_\"].dropna(), bins=72, range=(-np.pi, np.pi), color=\"tab:orange\", alpha=0.7)\n",
|
|
383
|
+
"ax.set_xlabel(\"Turning angle (radians)\")\n",
|
|
384
|
+
"ax.set_ylabel(\"Count\")\n",
|
|
385
|
+
"ax.set_xticks([-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi])\n",
|
|
386
|
+
"ax.set_xticklabels([\"-π\", \"-π/2\", \"0\", \"π/2\", \"π\"])\n",
|
|
387
|
+
"ax.set_xlim(-np.pi, np.pi)\n",
|
|
388
|
+
"plt.tight_layout()\n",
|
|
389
|
+
"plt.show()"
|
|
390
|
+
]
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"cell_type": "markdown",
|
|
394
|
+
"metadata": {},
|
|
395
|
+
"source": [
|
|
396
|
+
"### Turning angles through time\n",
|
|
397
|
+
"\n",
|
|
398
|
+
"As turning angles are circular, the mean/median sit\n",
|
|
399
|
+
"near 0 regardless of spread. Instead, we summarise each hour with the circular SD \n",
|
|
400
|
+
"and the shortest arc containing 50% of the angles (which may wrap across ±π), i.e.,\n",
|
|
401
|
+
"where are the densest 50% of the turning angles concentrated?\n",
|
|
402
|
+
"\n",
|
|
403
|
+
"From this plot, we can see that movements are most directed (mean at 0 and most TAs\n",
|
|
404
|
+
"near 0) around dawn and dusk, reflecting the longer steps in those periods. \n",
|
|
405
|
+
"This shows that buffalo appear to be travelling in a directed way during those periods. "
|
|
406
|
+
]
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"cell_type": "code",
|
|
410
|
+
"execution_count": null,
|
|
411
|
+
"metadata": {},
|
|
412
|
+
"outputs": [],
|
|
413
|
+
"source": [
|
|
414
|
+
"# create a helper function to summarise turning angles by hour of day \n",
|
|
415
|
+
"# into circular SD and the shortest arc containing 50% of the angles\n",
|
|
416
|
+
"def circ_summary(theta, prob=0.5):\n",
|
|
417
|
+
" \"\"\"Circular SD and the shortest arc containing `prob` of the angles.\"\"\"\n",
|
|
418
|
+
" theta = np.sort(np.asarray(theta))\n",
|
|
419
|
+
" n = len(theta)\n",
|
|
420
|
+
" circ_sd = np.sqrt(-2 * np.log(np.abs(np.mean(np.exp(1j * theta)))))\n",
|
|
421
|
+
" k = int(np.ceil(prob * n))\n",
|
|
422
|
+
" ext = np.concatenate([theta, theta + 2 * np.pi]) # unwrap so arcs can cross ±π\n",
|
|
423
|
+
" widths = ext[k - 1 : k - 1 + n] - ext[:n]\n",
|
|
424
|
+
" i = np.argmin(widths)\n",
|
|
425
|
+
" return pd.Series({\"circ_sd\": circ_sd, \"arc_start\": ext[i], \"arc_end\": ext[i + k - 1]})\n",
|
|
426
|
+
"\n",
|
|
427
|
+
"ta_df = step_df.dropna(subset=[\"ta_\"])\n",
|
|
428
|
+
"hourly_ta = ta_df.groupby(ta_df[\"hour_t1\"].round().mod(24))[\"ta_\"].apply(circ_summary).unstack()\n",
|
|
429
|
+
"\n",
|
|
430
|
+
"fig, (ax, ax_sd) = plt.subplots(2, 1, figsize=(8, 6), sharex=True, height_ratios=[3, 1])\n",
|
|
431
|
+
"ax.scatter(ta_df[\"hour_t1\"], ta_df[\"ta_\"], s=4, alpha=0.15, color=\"tab:orange\", label=\"Steps\")\n",
|
|
432
|
+
"for hour, row in hourly_ta.iterrows():\n",
|
|
433
|
+
" # draw the arc as one segment, or two if it wraps past π\n",
|
|
434
|
+
" segments = [(row.arc_start, min(row.arc_end, np.pi))]\n",
|
|
435
|
+
" if row.arc_end > np.pi:\n",
|
|
436
|
+
" segments.append((-np.pi, row.arc_end - 2 * np.pi))\n",
|
|
437
|
+
" for lo, hi in segments:\n",
|
|
438
|
+
" ax.vlines(hour, lo, hi, color=\"black\", lw=3, label=\"Densest 50%\" if hour == hourly_ta.index[0] and lo == row.arc_start else None)\n",
|
|
439
|
+
"ax.set_ylabel(\"Turning angle (radians)\")\n",
|
|
440
|
+
"ax.set_yticks([-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi])\n",
|
|
441
|
+
"ax.set_yticklabels([\"-π\", \"-π/2\", \"0\", \"π/2\", \"π\"])\n",
|
|
442
|
+
"ax.set_ylim(-np.pi, np.pi)\n",
|
|
443
|
+
"ax.legend(loc=\"upper right\")\n",
|
|
444
|
+
"\n",
|
|
445
|
+
"ax_sd.plot(hourly_ta.index, hourly_ta[\"circ_sd\"], color=\"black\", lw=2, marker=\"o\", ms=4)\n",
|
|
446
|
+
"ax_sd.set_ylabel(\"Circular SD\")\n",
|
|
447
|
+
"ax_sd.set_xlabel(\"Hour of day\")\n",
|
|
448
|
+
"ax_sd.set_xticks(range(0, 25, 3))\n",
|
|
449
|
+
"ax_sd.set_xlim(0, 24)\n",
|
|
450
|
+
"plt.tight_layout()\n",
|
|
451
|
+
"plt.show()"
|
|
452
|
+
]
|
|
453
|
+
},
|
|
264
454
|
{
|
|
265
455
|
"cell_type": "markdown",
|
|
266
456
|
"metadata": {},
|
|
@@ -1552,17 +1742,29 @@
|
|
|
1552
1742
|
"metadata": {},
|
|
1553
1743
|
"outputs": [],
|
|
1554
1744
|
"source": [
|
|
1555
|
-
"#
|
|
1556
|
-
"# step as the location keeps the two consistent — the diel and seasonal\n",
|
|
1557
|
-
"# covariates then describe a moment the animal really was at that place.\n",
|
|
1745
|
+
"# the starting id of the step (should be 1)\n",
|
|
1558
1746
|
"START_IDX = 1\n",
|
|
1747
|
+
"\n",
|
|
1748
|
+
"# where should the simulation start? \n",
|
|
1749
|
+
"# here we use the first step in the observed data, but any valid (x, y) within the\n",
|
|
1750
|
+
"# raster extent could be used. \n",
|
|
1559
1751
|
"start_x = step_df[\"x1_\"].iloc[START_IDX]\n",
|
|
1560
1752
|
"start_y = step_df[\"y1_\"].iloc[START_IDX]\n",
|
|
1561
|
-
"
|
|
1562
|
-
"
|
|
1563
|
-
"
|
|
1564
|
-
"
|
|
1565
|
-
"
|
|
1753
|
+
"print(f\"Starting location : ({start_x:.0f}, {start_y:.0f})\")\n",
|
|
1754
|
+
"\n",
|
|
1755
|
+
"# the timestamp of the first step in the observed data\n",
|
|
1756
|
+
"START_TIME = step_df[\"t1_\"].iloc[START_IDX]\n",
|
|
1757
|
+
"# extract the day of year and hour of day as floats for the simulation inputs\n",
|
|
1758
|
+
"START_YDAY = START_TIME.timetuple().tm_yday\n",
|
|
1759
|
+
"START_HOUR = START_TIME.hour + START_TIME.minute / 60.0\n",
|
|
1760
|
+
"\n",
|
|
1761
|
+
"print(f\"Starting timestamp : {START_TIME}\")\n",
|
|
1762
|
+
"print(f\"Starting time : day {START_YDAY:.0f}, {START_HOUR:.2f} h\")\n",
|
|
1763
|
+
"\n",
|
|
1764
|
+
"# hours between simulated steps - this should ideally be the same as the observed step interval, \n",
|
|
1765
|
+
"# but the model can be used to simulate at any interval\n",
|
|
1766
|
+
"# (but it may not have learned a meaningful movement kernel for other intervals).\n",
|
|
1767
|
+
"STEP_INTERVAL = 1.0 "
|
|
1566
1768
|
]
|
|
1567
1769
|
},
|
|
1568
1770
|
{
|
|
@@ -1595,6 +1797,8 @@
|
|
|
1595
1797
|
" - starting day-of-year \n",
|
|
1596
1798
|
" * `starting_hour`\n",
|
|
1597
1799
|
" - starting hour-of-day\n",
|
|
1800
|
+
" * `start_time`\n",
|
|
1801
|
+
" - what time should the simulation start\n",
|
|
1598
1802
|
" * `time_between_steps`\n",
|
|
1599
1803
|
" - time between steps in hours\n",
|
|
1600
1804
|
" * `window_size`\n",
|
|
@@ -1626,7 +1830,8 @@
|
|
|
1626
1830
|
" n_steps=1000,\n",
|
|
1627
1831
|
" starting_yday=START_YDAY,\n",
|
|
1628
1832
|
" starting_hour=START_HOUR,\n",
|
|
1629
|
-
"
|
|
1833
|
+
" start_time=START_TIME,\n",
|
|
1834
|
+
" time_between_steps=STEP_INTERVAL,\n",
|
|
1630
1835
|
" window_size=WINDOW_SIZE,\n",
|
|
1631
1836
|
" month_index_fn=lambda _yday: 0, # static layers\n",
|
|
1632
1837
|
")\n",
|
|
@@ -1752,6 +1957,7 @@
|
|
|
1752
1957
|
"metadata": {},
|
|
1753
1958
|
"outputs": [],
|
|
1754
1959
|
"source": [
|
|
1960
|
+
"# set up a directory to hold the CSV files for the simulated trajectories\n",
|
|
1755
1961
|
"TRAJ_DIR = OUTPUT_DIR / \"deepSSF_trajectories\"\n",
|
|
1756
1962
|
"\n",
|
|
1757
1963
|
"# save the simulated trajectory to a CSV file\n",
|
|
@@ -1802,6 +2008,7 @@
|
|
|
1802
2008
|
" method=\"batched\",\n",
|
|
1803
2009
|
" starting_yday=START_YDAY,\n",
|
|
1804
2010
|
" starting_hour=START_HOUR,\n",
|
|
2011
|
+
" start_time=START_TIME,\n",
|
|
1805
2012
|
" time_between_steps=1.0,\n",
|
|
1806
2013
|
" window_size=WINDOW_SIZE,\n",
|
|
1807
2014
|
" month_index_fn=lambda _yday: 0, # static layers\n",
|
|
@@ -1855,6 +2062,7 @@
|
|
|
1855
2062
|
" method=\"batched\",\n",
|
|
1856
2063
|
" starting_yday=START_YDAY,\n",
|
|
1857
2064
|
" starting_hour=START_HOUR,\n",
|
|
2065
|
+
" start_time=START_TIME,\n",
|
|
1858
2066
|
" window_size=WINDOW_SIZE,\n",
|
|
1859
2067
|
" month_index_fn=lambda _yday: 0,\n",
|
|
1860
2068
|
")\n",
|
|
@@ -1883,6 +2091,26 @@
|
|
|
1883
2091
|
"# save_trajectories(sim_many, TRAJ_DIR, prefix=\"deepSSF_pigs\", split=True)"
|
|
1884
2092
|
]
|
|
1885
2093
|
},
|
|
2094
|
+
{
|
|
2095
|
+
"cell_type": "markdown",
|
|
2096
|
+
"metadata": {},
|
|
2097
|
+
"source": [
|
|
2098
|
+
"### To read in previously saved trajectories"
|
|
2099
|
+
]
|
|
2100
|
+
},
|
|
2101
|
+
{
|
|
2102
|
+
"cell_type": "code",
|
|
2103
|
+
"execution_count": null,
|
|
2104
|
+
"metadata": {},
|
|
2105
|
+
"outputs": [],
|
|
2106
|
+
"source": [
|
|
2107
|
+
"# # to read previously run simulations from disk, rather than re-running the simulation above\n",
|
|
2108
|
+
"# SIM_PATH = TRAJ_DIR / \"deepSSF_buffalo_scattered_1000traj_1000steps_2026-10-22.csv\"\n",
|
|
2109
|
+
"# sim_scattered = pd.read_csv(SIM_PATH)\n",
|
|
2110
|
+
"# print(f\"Read {len(sim_scattered):,} locations from {SIM_PATH}\")\n",
|
|
2111
|
+
"# sim_scattered.head()"
|
|
2112
|
+
]
|
|
2113
|
+
},
|
|
1886
2114
|
{
|
|
1887
2115
|
"cell_type": "markdown",
|
|
1888
2116
|
"metadata": {},
|
|
@@ -1960,6 +2188,220 @@
|
|
|
1960
2188
|
"fmap_traj"
|
|
1961
2189
|
]
|
|
1962
2190
|
},
|
|
2191
|
+
{
|
|
2192
|
+
"cell_type": "markdown",
|
|
2193
|
+
"metadata": {},
|
|
2194
|
+
"source": [
|
|
2195
|
+
"## Visualise some data summaries of the simulated trajectories\n",
|
|
2196
|
+
"\n",
|
|
2197
|
+
"Here we can check the summaries of the observed data against summaries of the simulated trajectories,\n",
|
|
2198
|
+
"to see whether we've captured those behaviours.\n",
|
|
2199
|
+
"\n",
|
|
2200
|
+
"### Calculate step lengths and turning angles from the simulated data"
|
|
2201
|
+
]
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
2204
|
+
"cell_type": "code",
|
|
2205
|
+
"execution_count": null,
|
|
2206
|
+
"metadata": {},
|
|
2207
|
+
"outputs": [],
|
|
2208
|
+
"source": [
|
|
2209
|
+
"sim_scattered"
|
|
2210
|
+
]
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
"cell_type": "code",
|
|
2214
|
+
"execution_count": null,
|
|
2215
|
+
"metadata": {},
|
|
2216
|
+
"outputs": [],
|
|
2217
|
+
"source": [
|
|
2218
|
+
"step_df = deepssf.prepare_movement_df(\n",
|
|
2219
|
+
" sim_scattered, \n",
|
|
2220
|
+
" id_col=\"trajectory_id\", \n",
|
|
2221
|
+
" time_col=\"time\", \n",
|
|
2222
|
+
" x_col=\"x\", \n",
|
|
2223
|
+
" y_col=\"y\"\n",
|
|
2224
|
+
")\n",
|
|
2225
|
+
"\n",
|
|
2226
|
+
"n_dropped = len(raw_df) - len(step_df)\n",
|
|
2227
|
+
"print(f\"Movement steps : {len(step_df):,} ({n_dropped} dropped — last per individual)\")\n",
|
|
2228
|
+
"step_df.head()"
|
|
2229
|
+
]
|
|
2230
|
+
},
|
|
2231
|
+
{
|
|
2232
|
+
"cell_type": "markdown",
|
|
2233
|
+
"metadata": {},
|
|
2234
|
+
"source": [
|
|
2235
|
+
"### Distribution of step lengths"
|
|
2236
|
+
]
|
|
2237
|
+
},
|
|
2238
|
+
{
|
|
2239
|
+
"cell_type": "code",
|
|
2240
|
+
"execution_count": null,
|
|
2241
|
+
"metadata": {},
|
|
2242
|
+
"outputs": [],
|
|
2243
|
+
"source": [
|
|
2244
|
+
"# Distribution of step lengths, with the median and mean marked\n",
|
|
2245
|
+
"fig, ax = plt.subplots(figsize=(8, 4))\n",
|
|
2246
|
+
"# observed step lengths\n",
|
|
2247
|
+
"# ax.hist(step_df[\"sl_\"], bins=250, color=\"tab:blue\", alpha=0.7)\n",
|
|
2248
|
+
"# ax.axvline(step_df[\"sl_\"].median(), color=\"black\", lw=2, label=\"Median\")\n",
|
|
2249
|
+
"# ax.axvline(step_df[\"sl_\"].mean(), color=\"black\", lw=2, ls=\"--\", label=\"Mean\")\n",
|
|
2250
|
+
"# simulated step lengths\n",
|
|
2251
|
+
"ax.hist(sim_scattered[\"sl_\"], bins=250, color=\"tab:blue\", alpha=0.7)\n",
|
|
2252
|
+
"ax.axvline(sim_scattered[\"sl_\"].median(), color=\"black\", lw=2, label=\"Median\")\n",
|
|
2253
|
+
"ax.axvline(sim_scattered[\"sl_\"].mean(), color=\"black\", lw=2, ls=\"--\", label=\"Mean\")\n",
|
|
2254
|
+
"ax.set_xlabel(\"Step length (m)\")\n",
|
|
2255
|
+
"ax.set_ylabel(\"Count\")\n",
|
|
2256
|
+
"ax.set_xlim(0, 2000)\n",
|
|
2257
|
+
"ax.legend()\n",
|
|
2258
|
+
"plt.tight_layout()\n",
|
|
2259
|
+
"plt.show()"
|
|
2260
|
+
]
|
|
2261
|
+
},
|
|
2262
|
+
{
|
|
2263
|
+
"cell_type": "markdown",
|
|
2264
|
+
"metadata": {},
|
|
2265
|
+
"source": [
|
|
2266
|
+
"### Log of step lengths"
|
|
2267
|
+
]
|
|
2268
|
+
},
|
|
2269
|
+
{
|
|
2270
|
+
"cell_type": "code",
|
|
2271
|
+
"execution_count": null,
|
|
2272
|
+
"metadata": {},
|
|
2273
|
+
"outputs": [],
|
|
2274
|
+
"source": [
|
|
2275
|
+
"# Distribution of log step lengths, with the median and mean marked\n",
|
|
2276
|
+
"fig, ax = plt.subplots(figsize=(8, 4))\n",
|
|
2277
|
+
"ax.hist(np.log(step_df[\"sl_\"]), bins=250, color=\"tab:blue\", alpha=0.7)\n",
|
|
2278
|
+
"ax.axvline(np.log(step_df[\"sl_\"].median()), color=\"black\", lw=2, label=\"Median\")\n",
|
|
2279
|
+
"ax.axvline(np.log(step_df[\"sl_\"].mean()), color=\"black\", lw=2, ls=\"--\", label=\"Mean\")\n",
|
|
2280
|
+
"ax.set_xlabel(\"Log step length (m)\")\n",
|
|
2281
|
+
"ax.set_ylabel(\"Count\")\n",
|
|
2282
|
+
"ax.legend()\n",
|
|
2283
|
+
"plt.tight_layout()\n",
|
|
2284
|
+
"plt.show()"
|
|
2285
|
+
]
|
|
2286
|
+
},
|
|
2287
|
+
{
|
|
2288
|
+
"cell_type": "markdown",
|
|
2289
|
+
"metadata": {},
|
|
2290
|
+
"source": [
|
|
2291
|
+
"### Step lengths through time"
|
|
2292
|
+
]
|
|
2293
|
+
},
|
|
2294
|
+
{
|
|
2295
|
+
"cell_type": "code",
|
|
2296
|
+
"execution_count": null,
|
|
2297
|
+
"metadata": {},
|
|
2298
|
+
"outputs": [],
|
|
2299
|
+
"source": [
|
|
2300
|
+
"# Step length by hour of day, with the hourly median and mean overlaid\n",
|
|
2301
|
+
"hourly_sl_median = step_df.groupby(step_df[\"hour_t1\"].round().mod(24))[\"sl_\"].median()\n",
|
|
2302
|
+
"hourly_sl_mean = step_df.groupby(step_df[\"hour_t1\"].round().mod(24))[\"sl_\"].mean()\n",
|
|
2303
|
+
"\n",
|
|
2304
|
+
"fig, ax = plt.subplots(figsize=(8, 4))\n",
|
|
2305
|
+
"ax.scatter(step_df[\"hour_t1\"], step_df[\"sl_\"], s=4, alpha=0.15, color=\"tab:blue\", label=\"Steps\")\n",
|
|
2306
|
+
"ax.plot(hourly_sl_mean.index, hourly_sl_mean.values, color=\"red\", lw=2, marker=\"s\", ms=4, label=\"Hourly mean\")\n",
|
|
2307
|
+
"ax.plot(hourly_sl_median.index, hourly_sl_median.values, color=\"black\", lw=2, marker=\"o\", ms=4, label=\"Hourly median\")\n",
|
|
2308
|
+
"ax.set_xlabel(\"Hour of day\")\n",
|
|
2309
|
+
"ax.set_ylabel(\"Step length (m)\")\n",
|
|
2310
|
+
"ax.set_xticks(range(0, 25, 3))\n",
|
|
2311
|
+
"ax.set_xlim(0, 24)\n",
|
|
2312
|
+
"ax.set_ylim(0, 1000)\n",
|
|
2313
|
+
"ax.legend()\n",
|
|
2314
|
+
"plt.tight_layout()\n",
|
|
2315
|
+
"plt.show()"
|
|
2316
|
+
]
|
|
2317
|
+
},
|
|
2318
|
+
{
|
|
2319
|
+
"cell_type": "markdown",
|
|
2320
|
+
"metadata": {},
|
|
2321
|
+
"source": [
|
|
2322
|
+
"### Distribution of turning angles"
|
|
2323
|
+
]
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
"cell_type": "code",
|
|
2327
|
+
"execution_count": null,
|
|
2328
|
+
"metadata": {},
|
|
2329
|
+
"outputs": [],
|
|
2330
|
+
"source": [
|
|
2331
|
+
"# Distribution of turning angles (NaN for each individual's first step, so dropped)\n",
|
|
2332
|
+
"fig, ax = plt.subplots(figsize=(8, 4))\n",
|
|
2333
|
+
"ax.hist(step_df[\"ta_\"].dropna(), bins=72, range=(-np.pi, np.pi), color=\"tab:orange\", alpha=0.7)\n",
|
|
2334
|
+
"ax.set_xlabel(\"Turning angle (radians)\")\n",
|
|
2335
|
+
"ax.set_ylabel(\"Count\")\n",
|
|
2336
|
+
"ax.set_xticks([-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi])\n",
|
|
2337
|
+
"ax.set_xticklabels([\"-π\", \"-π/2\", \"0\", \"π/2\", \"π\"])\n",
|
|
2338
|
+
"ax.set_xlim(-np.pi, np.pi)\n",
|
|
2339
|
+
"plt.tight_layout()\n",
|
|
2340
|
+
"plt.show()"
|
|
2341
|
+
]
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
"cell_type": "markdown",
|
|
2345
|
+
"metadata": {},
|
|
2346
|
+
"source": [
|
|
2347
|
+
"### Turning angles through time\n",
|
|
2348
|
+
"\n",
|
|
2349
|
+
"As turning angles are circular, the mean/median sit\n",
|
|
2350
|
+
"near 0 regardless of spread. Instead, we summarise each hour with the circular SD \n",
|
|
2351
|
+
"and the shortest arc containing 50% of the angles (which may wrap across ±π), i.e.,\n",
|
|
2352
|
+
"where are the densest 50% of the turning angles concentrated?\n",
|
|
2353
|
+
"\n",
|
|
2354
|
+
"From this plot, we can see that movements are most directed (mean at 0 and most TAs\n",
|
|
2355
|
+
"near 0) around dawn and dusk, reflecting the longer steps in those periods. \n",
|
|
2356
|
+
"This shows that buffalo appear to be travelling in a directed way during those periods. "
|
|
2357
|
+
]
|
|
2358
|
+
},
|
|
2359
|
+
{
|
|
2360
|
+
"cell_type": "code",
|
|
2361
|
+
"execution_count": null,
|
|
2362
|
+
"metadata": {},
|
|
2363
|
+
"outputs": [],
|
|
2364
|
+
"source": [
|
|
2365
|
+
"# create a helper function to summarise turning angles by hour of day \n",
|
|
2366
|
+
"# into circular SD and the shortest arc containing 50% of the angles\n",
|
|
2367
|
+
"def circ_summary(theta, prob=0.5):\n",
|
|
2368
|
+
" \"\"\"Circular SD and the shortest arc containing `prob` of the angles.\"\"\"\n",
|
|
2369
|
+
" theta = np.sort(np.asarray(theta))\n",
|
|
2370
|
+
" n = len(theta)\n",
|
|
2371
|
+
" circ_sd = np.sqrt(-2 * np.log(np.abs(np.mean(np.exp(1j * theta)))))\n",
|
|
2372
|
+
" k = int(np.ceil(prob * n))\n",
|
|
2373
|
+
" ext = np.concatenate([theta, theta + 2 * np.pi]) # unwrap so arcs can cross ±π\n",
|
|
2374
|
+
" widths = ext[k - 1 : k - 1 + n] - ext[:n]\n",
|
|
2375
|
+
" i = np.argmin(widths)\n",
|
|
2376
|
+
" return pd.Series({\"circ_sd\": circ_sd, \"arc_start\": ext[i], \"arc_end\": ext[i + k - 1]})\n",
|
|
2377
|
+
"\n",
|
|
2378
|
+
"ta_df = step_df.dropna(subset=[\"ta_\"])\n",
|
|
2379
|
+
"hourly_ta = ta_df.groupby(ta_df[\"hour_t1\"].round().mod(24))[\"ta_\"].apply(circ_summary).unstack()\n",
|
|
2380
|
+
"\n",
|
|
2381
|
+
"fig, (ax, ax_sd) = plt.subplots(2, 1, figsize=(8, 6), sharex=True, height_ratios=[3, 1])\n",
|
|
2382
|
+
"ax.scatter(ta_df[\"hour_t1\"], ta_df[\"ta_\"], s=4, alpha=0.15, color=\"tab:orange\", label=\"Steps\")\n",
|
|
2383
|
+
"for hour, row in hourly_ta.iterrows():\n",
|
|
2384
|
+
" # draw the arc as one segment, or two if it wraps past π\n",
|
|
2385
|
+
" segments = [(row.arc_start, min(row.arc_end, np.pi))]\n",
|
|
2386
|
+
" if row.arc_end > np.pi:\n",
|
|
2387
|
+
" segments.append((-np.pi, row.arc_end - 2 * np.pi))\n",
|
|
2388
|
+
" for lo, hi in segments:\n",
|
|
2389
|
+
" ax.vlines(hour, lo, hi, color=\"black\", lw=3, label=\"Densest 50%\" if hour == hourly_ta.index[0] and lo == row.arc_start else None)\n",
|
|
2390
|
+
"ax.set_ylabel(\"Turning angle (radians)\")\n",
|
|
2391
|
+
"ax.set_yticks([-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi])\n",
|
|
2392
|
+
"ax.set_yticklabels([\"-π\", \"-π/2\", \"0\", \"π/2\", \"π\"])\n",
|
|
2393
|
+
"ax.set_ylim(-np.pi, np.pi)\n",
|
|
2394
|
+
"ax.legend(loc=\"upper right\")\n",
|
|
2395
|
+
"\n",
|
|
2396
|
+
"ax_sd.plot(hourly_ta.index, hourly_ta[\"circ_sd\"], color=\"black\", lw=2, marker=\"o\", ms=4)\n",
|
|
2397
|
+
"ax_sd.set_ylabel(\"Circular SD\")\n",
|
|
2398
|
+
"ax_sd.set_xlabel(\"Hour of day\")\n",
|
|
2399
|
+
"ax_sd.set_xticks(range(0, 25, 3))\n",
|
|
2400
|
+
"ax_sd.set_xlim(0, 24)\n",
|
|
2401
|
+
"plt.tight_layout()\n",
|
|
2402
|
+
"plt.show()"
|
|
2403
|
+
]
|
|
2404
|
+
},
|
|
1963
2405
|
{
|
|
1964
2406
|
"cell_type": "markdown",
|
|
1965
2407
|
"metadata": {},
|
|
@@ -1991,19 +2433,6 @@
|
|
|
1991
2433
|
"a layer that can be toggled on and off with `add_heatmap_overlay()`."
|
|
1992
2434
|
]
|
|
1993
2435
|
},
|
|
1994
|
-
{
|
|
1995
|
-
"cell_type": "code",
|
|
1996
|
-
"execution_count": null,
|
|
1997
|
-
"metadata": {},
|
|
1998
|
-
"outputs": [],
|
|
1999
|
-
"source": [
|
|
2000
|
-
"# # to read previously run simulations from disk, rather than re-running the simulation above\n",
|
|
2001
|
-
"# SIM_PATH = TRAJ_DIR / \"deepSSF_buffalo_scattered_100traj_1000steps_2026-10-22.csv\"\n",
|
|
2002
|
-
"# sim_scattered = pd.read_csv(SIM_PATH)\n",
|
|
2003
|
-
"# print(f\"Read {len(sim_scattered):,} locations from {SIM_PATH}\")\n",
|
|
2004
|
-
"# sim_scattered.head()"
|
|
2005
|
-
]
|
|
2006
|
-
},
|
|
2007
2436
|
{
|
|
2008
2437
|
"cell_type": "code",
|
|
2009
2438
|
"execution_count": null,
|