|
77 | 77 | "- You'll notice that each of the data functions have many unique inputs you can specify. **DO NOT** specify too many! Specify *just enough* inputs to return what you need. But do not provide redundant geographical or parameter information as this may slow down your query and lead to errors.\n", |
78 | 78 | "- Each function returns a Tuple, containing a dataframe and a Metadata class. If you have `geopandas` installed in your environment, the dataframe will be a `GeoDataFrame` with a geometry included. If you do not have `geopandas`, the dataframe will be a `pandas` dataframe with the geometry contained in a coordinates column. The Metadata object contains information about your query, like the query url.\n", |
79 | 79 | "- If you do not want to return the `geometry` column, use the input `skip_geometry=True`.\n", |
80 | | - "- All of these functions (except `get_samples()`) have a `limit` argument, which signifies the number of rows returned with each \"page\" of data. The Water Data APIs use paging to chunk up large responses and send data most efficiently to the requester. The `waterdata` functions collect the rows of data from each page and combine them into one final dataframe at the end. The default and maximum limit per page is 50,000 rows. In other words, if you request 100,000 rows of data from the database, it will return all the data in 2 pages, and each page counts as a \"request\" using your API key. If you were to change the argument to `limit=10000`, then each page returned would contain 10,000 rows, and it would take 10 requests/pages to return the total 100,000 rows. In general, there is no need to adjust the `limit` argument. However, if you are working with slow internet speeds, adjusting the `limit` argument may reduce chances of failures due to bandwidth." |
| 80 | + "- All of these functions (except `get_samples()`) have a `limit` argument, which signifies the number of rows returned with each \"page\" of data. The Water Data APIs use paging to chunk up large responses and send data most efficiently to the requester. The `waterdata` functions collect the rows of data from each page and combine them into one final dataframe at the end. The default and maximum limit per page is 50,000 rows. In other words, if you request 100,000 rows of data from the database, it will return all the data in 2 pages, and each page counts as a \"request\" using your API key. If you were to change the argument to `limit=10000`, then each page returned would contain 10,000 rows, and it would take 10 requests/pages to return the total 100,000 rows. In general, there is no need to adjust the `limit` argument. However, if you are working with slow internet speeds, adjusting the `limit` argument may reduce chances of failures due to bandwidth.\n", |
| 81 | + "- You can find some other helpful tips in the [Water Data API documentation](https://api.waterdata.usgs.gov/docs/ogcapi/)." |
81 | 82 | ] |
82 | 83 | }, |
83 | 84 | { |
|
86 | 87 | "metadata": {}, |
87 | 88 | "source": [ |
88 | 89 | "## Examples\n", |
89 | | - "Let's get into some examples using the functions listed above. First, we need to load the `waterdata` module and a few other packages and functions to go through the examples." |
| 90 | + "Let's get into some examples using the functions listed above. First, we need to load the `waterdata` module and a few other packages and functions to go through the examples. To run the entirety of this notebook, you will need to install `dataretrieval`, `matplotlib`, and `geopandas` packages. `matplotlib` is needed to create the plots, and `geopandas` is needed to create the interactive maps." |
90 | 91 | ] |
91 | 92 | }, |
92 | 93 | { |
|
104 | 105 | "from datetime import datetime, timedelta\n", |
105 | 106 | "from datetime import date\n", |
106 | 107 | "from dateutil.relativedelta import relativedelta\n", |
107 | | - "from dataretrieval import waterdata" |
| 108 | + "from dataretrieval import waterdata\n", |
| 109 | + "\n", |
| 110 | + "# Check if geopandas is installed\n", |
| 111 | + "import importlib.util\n", |
| 112 | + "if importlib.util.find_spec(\"geopandas\") is None:\n", |
| 113 | + " GEOPANDAS=False\n", |
| 114 | + "else:\n", |
| 115 | + " GEOPANDAS=True" |
108 | 116 | ] |
109 | 117 | }, |
110 | 118 | { |
|
176 | 184 | "one_week_ago = (datetime.now() - timedelta(days=7)).date().strftime(\"%Y-%m-%d\")" |
177 | 185 | ] |
178 | 186 | }, |
| 187 | + { |
| 188 | + "cell_type": "markdown", |
| 189 | + "id": "261b5a32", |
| 190 | + "metadata": {}, |
| 191 | + "source": [ |
| 192 | + "We will also use the `skip_geometry` argument in our timeseries metadata call. By default, most `waterdata` functions return a geometry column containing the monitoring location's coordinates. This is a really cool feature that we will use later, but for this particular data pull, we don't need it. Setting `skip_geometry=True` makes the returned dataframe smaller and more efficient." |
| 193 | + ] |
| 194 | + }, |
179 | 195 | { |
180 | 196 | "cell_type": "code", |
181 | 197 | "execution_count": null, |
|
208 | 224 | "id": "8f464470", |
209 | 225 | "metadata": {}, |
210 | 226 | "source": [ |
211 | | - "In the dataframe above, we are looking at 5 timeseries returned, ordered by monitoring location. You can also see that the first two rows show two different kinds of discharge for the same monitoring location: a mean daily discharge timeseries (with statistic id 00003, which represents \"mean\") and an instantaneous discharge timeseries (with statistic id 00011, which represents \"points\" or \"instantaneous\" values). Look closely and you may also notice that the `parent_timeseries_id` column for daily mean discharge matches the `time_series_id` for the instantaneous discharge. This is because once instantaneous measurements began at the site, they were used to calculate the daily mean." |
| 227 | + "In the dataframe above, we are looking at 5 timeseries returned, ordered by monitoring location. You can also see that the first two rows show two different kinds of discharge for the same monitoring location: a mean daily discharge timeseries (with [statistic id](https://api.waterdata.usgs.gov/docs/ogcapi/) 00003, which represents \"mean\") and an instantaneous discharge timeseries (with statistic id 00011, which represents \"points\" or \"instantaneous\" values). Look closely and you may also notice that the `parent_timeseries_id` column for daily mean discharge matches the `time_series_id` for the instantaneous discharge. This is because once instantaneous measurements began at the site, they were used to calculate the daily mean." |
212 | 228 | ] |
213 | 229 | }, |
214 | 230 | { |
|
217 | 233 | "metadata": {}, |
218 | 234 | "source": [ |
219 | 235 | "### Monitoring locations\n", |
220 | | - "Now that we know which sites have recent discharge data, let's find stream sites and plot them on a map. We will use the `waterdata.get_monitoring_locations()` function to grab more metadata about these sites. Even though we have a list of monitoring location IDs from the timeseries function, it's faster to use the `state_name` argument to return all stream sites, and then filter down to the ones we're interested in." |
| 236 | + "Now that we know which sites have recent discharge data, let's find stream sites and plot them on a map. We will use the `waterdata.get_monitoring_locations()` function to grab more metadata about these sites.\n", |
| 237 | + "\n", |
| 238 | + "We can feed the unique monitoring location IDs from `NE_discharge` into the `get_monitoring_locations()` function to get the metadata for just those sites. However, there is a limit to the number of IDs that can be passed in one call to the API. The function may be able to handle the ~100 sites in one go, but for demonstration purposes, we will split the list of monitoring location IDs into a few chunks of 50 sent to the API and stitch the resulting dataframes together. Further down in this notebook, you'll see an example where we successfully feed all ~100 IDs in one call to the API. A loose rule of thumb is to keep the number of IDs below 200, but this exact number will depend on the typical length of each monitoring location ID (i.e. if your monitoring location IDs are > 13 characters long: \"USGS-XXXXXXXX\"+, you will need to feed in less than 200 at a time)." |
| 239 | + ] |
| 240 | + }, |
| 241 | + { |
| 242 | + "cell_type": "code", |
| 243 | + "execution_count": null, |
| 244 | + "id": "3c3eeac3", |
| 245 | + "metadata": {}, |
| 246 | + "outputs": [], |
| 247 | + "source": [ |
| 248 | + "chunk_size=50\n", |
| 249 | + "site_list = NE_discharge['monitoring_location_id'].unique().tolist()\n", |
| 250 | + "chunks = [site_list[i:i + chunk_size] for i in range(0, len(site_list), chunk_size)]\n", |
| 251 | + "NE_locations = pd.DataFrame()\n", |
| 252 | + "for site_group in chunks:\n", |
| 253 | + " try:\n", |
| 254 | + " chunk_data,_ = waterdata.get_monitoring_locations(\n", |
| 255 | + " monitoring_location_id=site_group,\n", |
| 256 | + " site_type_code=\"ST\"\n", |
| 257 | + " )\n", |
| 258 | + " if not chunk_data.empty:\n", |
| 259 | + " NE_locations = pd.concat([NE_locations, chunk_data])\n", |
| 260 | + " except Exception as e:\n", |
| 261 | + " print(f\"Chunk failed: {e}\")\n", |
| 262 | + "\n", |
| 263 | + "display(NE_locations[[\"monitoring_location_id\", \"monitoring_location_name\", \"hydrologic_unit_code\"]].head())" |
| 264 | + ] |
| 265 | + }, |
| 266 | + { |
| 267 | + "cell_type": "markdown", |
| 268 | + "id": "21a0f28f", |
| 269 | + "metadata": {}, |
| 270 | + "source": [ |
| 271 | + "That took a little bit of work to loop through the site chunks and bind the data back together. Admittedly, there may be times where chunking and iterating might be the most efficient workflow. But in this particular case, we have a less onerous option available: `get_monitoring_locations()` has a `state_name` argument. It will likely be faster to pull all stream sites for Nebraska and then filter down to the sites present in the timeseries dataframe: no iteration needed. Let's try this too." |
221 | 272 | ] |
222 | 273 | }, |
223 | 274 | { |
|
241 | 292 | "id": "f0fe5c4e", |
242 | 293 | "metadata": {}, |
243 | 294 | "source": [ |
244 | | - "If you have `geopandas` installed, the function will return a `GeoDataFrame` with a `geometry` column containing the monitoring locations' coordinates. If you don't have `geopandas` installed, it will return a regular `pandas` DataFrame with coordinate columns instead. Let's take a look at the site locations using `gpd.explore()`. Hover over the site points to see all the columns returned from `waterdata.get_monitoring_locations()`." |
| 295 | + "If you have `geopandas` installed, the function will return a `GeoDataFrame` with a `geometry` column containing the monitoring locations' coordinates. You can use `gpd.explore()` to view your geometry coordinates on an interactive map. We will demo this functionality below (Hover over the site points to see all the columns returned from `waterdata.get_monitoring_locations()`). If you don't have `geopandas` installed, `dataretrieval` will return a regular `pandas` DataFrame with coordinate columns instead." |
245 | 296 | ] |
246 | 297 | }, |
247 | 298 | { |
|
251 | 302 | "metadata": {}, |
252 | 303 | "outputs": [], |
253 | 304 | "source": [ |
254 | | - "NE_locations_discharge.set_crs(crs=\"WGS84\").explore()" |
| 305 | + "if GEOPANDAS:\n", |
| 306 | + " NE_locations_discharge.set_crs(crs=\"WGS84\").explore()" |
255 | 307 | ] |
256 | 308 | }, |
257 | 309 | { |
|
295 | 347 | "metadata": {}, |
296 | 348 | "outputs": [], |
297 | 349 | "source": [ |
298 | | - "latest_dv['date'] = latest_dv['time'].astype(str)\n", |
299 | | - "\n", |
300 | | - "latest_dv[['geometry', 'monitoring_location_id', 'date', 'value', 'unit_of_measure']].set_crs(crs=\"WGS84\").explore(column='value', tiles='CartoDB dark matter', cmap='YlOrRd', scheme=None, legend=True)" |
| 350 | + "if GEOPANDAS:\n", |
| 351 | + " latest_dv['date'] = latest_dv['time'].astype(str)\n", |
| 352 | + " latest_dv[['geometry', 'monitoring_location_id', 'date', 'value', 'unit_of_measure']].set_crs(crs=\"WGS84\").explore(column='value', tiles='CartoDB dark matter', cmap='YlOrRd', scheme=None, legend=True)" |
301 | 353 | ] |
302 | 354 | }, |
303 | 355 | { |
|
320 | 372 | " parameter_code=\"00060\",\n", |
321 | 373 | " statistic_id=\"00011\"\n", |
322 | 374 | ")\n", |
323 | | - "latest_instantaneous['datetime'] = latest_instantaneous['time'].astype(str)\n", |
324 | 375 | "\n", |
325 | | - "latest_instantaneous[['geometry', 'monitoring_location_id', 'datetime', 'value', 'unit_of_measure']].set_crs(crs=\"WGS84\").explore(column='value', cmap='YlOrRd', scheme=None, legend=True)" |
| 376 | + "if GEOPANDAS:\n", |
| 377 | + " latest_instantaneous['datetime'] = latest_instantaneous['time'].astype(str)\n", |
| 378 | + "\n", |
| 379 | + " latest_instantaneous[['geometry', 'monitoring_location_id', 'datetime', 'value', 'unit_of_measure']].set_crs(crs=\"WGS84\").explore(column='value', cmap='YlOrRd', scheme=None, legend=True)" |
326 | 380 | ] |
327 | 381 | }, |
328 | 382 | { |
|
546 | 600 | "fig.suptitle(f\"Missouri River sites - Daily Mean, Instantaneous, and Field Measurement Discharge\")\n", |
547 | 601 | "fig.autofmt_xdate()\n" |
548 | 602 | ] |
| 603 | + }, |
| 604 | + { |
| 605 | + "cell_type": "markdown", |
| 606 | + "id": "60a1b100", |
| 607 | + "metadata": {}, |
| 608 | + "source": [ |
| 609 | + "## Additional Resources\n", |
| 610 | + "Check out the links below for more information on the Water Data APIs and other ways to download USGS water data:\n" |
| 611 | + ] |
549 | 612 | } |
550 | 613 | ], |
551 | 614 | "metadata": { |
552 | 615 | "kernelspec": { |
553 | | - "display_name": "dr-test", |
| 616 | + "display_name": "drpy-no-geopandas", |
554 | 617 | "language": "python", |
555 | 618 | "name": "python3" |
556 | 619 | }, |
|
564 | 627 | "name": "python", |
565 | 628 | "nbconvert_exporter": "python", |
566 | 629 | "pygments_lexer": "ipython3", |
567 | | - "version": "3.14.0" |
| 630 | + "version": "3.14.2" |
568 | 631 | } |
569 | 632 | }, |
570 | 633 | "nbformat": 4, |
|
0 commit comments