10-Year Interactive Drought and Precipitation Analysis for South Dakota, Pine Ridge, and Rosebud
Author
Antigravity Coding Assistant
Published
July 19, 2026
Introduction
In this document, we visualize the weekly drought records for Oglala Lakota County and Todd County alongside the daily precipitation records for South Dakota using Python’s Plotly library, providing interactive and responsive dashboards. These counties correspond to part of the Pine Ridge and Rosebud reservations and are labeled as such in plots.
We will display: 1. Both county drought datasets and the statewide rolling precipitation plotted over time (10-year timeline). 2. Time within the year (standardized month, X-axis) vs. annual cumulative values (Y-axis) for cumulative precipitation and the two counties’ cumulative drought indices. 3. Annual trajectories comparing cumulative precipitation (X-axis) vs. cumulative drought index (Y-axis) faceted side-by-side by county.
Data Processing & Alignment
First, we load the collected datasets, calculate the Drought Severity and Coverage Index (DSCI), compute cumulative precipitation, and merge the tables on matching dates.
DSCI Formula: For cumulative USDM statistics, the index is calculated as DSCI = D0 + D1 + D2 + D3 + D4 (range: 0 to 500).
Date Matching: We align the daily precipitation and weekly drought records on the weekly ValidStart date of each USDM report.
This faceted timeline displays the 10-year history of the weekly drought index (DSCI) for both counties alongside the daily South Dakota precipitation rolling average.
2. Visualizing Annual Cumulative Progression Over the Year (Time vs. Cumulative Values)
Here we plot time within the year (standardized month, X-axis) against cumulative values (Y-axis).
This contains 3 panels: - Cumulative Precipitation (South Dakota Mean) - Cumulative Drought Index (Pine Ridge) - Cumulative Drought Index (Rosebud)
Each year is shown as a separate colored curve in a rainbow series, with open circles indicating the end of each month. Click on a year in the legend to toggle its visibility across all subplots.
Code
import plotly.express as px# Identify colors for each year in a rainbow series (turbo colorscale)years =sorted(df_merged_oglala["year_str"].unique())colors = px.colors.sample_colorscale("turbo", [i / (len(years) -1) for i inrange(len(years))])year_colors =dict(zip(years, colors))fig_cum_time = make_subplots( rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.06, subplot_titles=("Cumulative Precipitation (South Dakota Mean)", "Cumulative Drought Index (Pine Ridge)", "Cumulative Drought Index (Rosebud)" ))# Identify month-end markersdf_month_ends_oglala = df_merged_oglala.groupby(["year", df_merged_oglala["date"].dt.month]).tail(1).copy()df_month_ends_oglala["day_in_year"] = pd.to_datetime(df_month_ends_oglala["date"].dt.strftime("2020-%m-%d"))df_month_ends_todd = df_merged_todd.groupby(["year", df_merged_todd["date"].dt.month]).tail(1).copy()df_month_ends_todd["day_in_year"] = pd.to_datetime(df_month_ends_todd["date"].dt.strftime("2020-%m-%d"))for year in years:# 1. Cumulative Precipitation df_y_p = df_merged_oglala[df_merged_oglala["year_str"] == year] df_pts_p = df_month_ends_oglala[df_month_ends_oglala["year_str"] == year] fig_cum_time.add_trace( go.Scatter( x=df_y_p["day_in_year"], y=df_y_p["cumulative_rain_year"], mode="lines", line=dict(color=year_colors[year]), name=year, legendgroup=year, showlegend=True ), row=1, col=1 ) fig_cum_time.add_trace( go.Scatter( x=df_pts_p["day_in_year"], y=df_pts_p["cumulative_rain_year"], mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), name=year, legendgroup=year, showlegend=False ), row=1, col=1 )# 2. Pine Ridge DSCI df_y_o = df_merged_oglala[df_merged_oglala["year_str"] == year] df_pts_o = df_month_ends_oglala[df_month_ends_oglala["year_str"] == year] fig_cum_time.add_trace( go.Scatter( x=df_y_o["day_in_year"], y=df_y_o["cumulative_dsci_year"], mode="lines", line=dict(color=year_colors[year]), name=year, legendgroup=year, showlegend=False ), row=2, col=1 ) fig_cum_time.add_trace( go.Scatter( x=df_pts_o["day_in_year"], y=df_pts_o["cumulative_dsci_year"], mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), name=year, legendgroup=year, showlegend=False ), row=2, col=1 )# 3. Rosebud DSCI df_y_t = df_merged_todd[df_merged_todd["year_str"] == year] df_pts_t = df_month_ends_todd[df_month_ends_todd["year_str"] == year] fig_cum_time.add_trace( go.Scatter( x=df_y_t["day_in_year"], y=df_y_t["cumulative_dsci_year"], mode="lines", line=dict(color=year_colors[year]), name=year, legendgroup=year, showlegend=False ), row=3, col=1 ) fig_cum_time.add_trace( go.Scatter( x=df_pts_t["day_in_year"], y=df_pts_t["cumulative_dsci_year"], mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), name=year, legendgroup=year, showlegend=False ), row=3, col=1 )fig_cum_time.update_layout( height =880, template ="plotly_white", title =dict( text ="Annual Progression of Cumulative Climate Metrics", y =0.95, x =0.5, xanchor ="center", yanchor ="top" ), margin =dict(t =90), legend =dict(# groupclick="togglegroup", <-- You can replace or keep this, but the below are safer for multi-subplots# itemclick = "toggle3", # Single-click toggles the entire group# itemdoubleclick = "isolate3", # Double-click isolates the entire group groupclick="togglegroup", # Natively validated to handle groups on double-click title_text ="Year" ), updatemenus=[dict(type="buttons", direction="right", x=1, y=0, xanchor="left", yanchor="bottom", showactive=False, buttons=[dict( label="Reset View", method="update", args=[ {"visible": [True] *len(fig_cum_time.data)}, {"xaxis.autorange": True,"yaxis.autorange": True,"yaxis2.autorange": True,"yaxis3.autorange": True } ] ) ] ) ])# Format the x-axesfig_cum_time.update_xaxes(tickformat="%b", dtick="M1")fig_cum_time.show()
3. Visualizing Trajectories of Cumulative Rain vs. Cumulative Drought Index (Faceted by County)
Finally, we plot annual cumulative precipitation (X-axis) against the annual cumulative drought index (DSCI) (Y-axis).
The curves for Pine Ridge and Rosebud are displayed side-by-side as two facets on the same plot surface. Individual years are colored in a rainbow series with open circles marking the end of each month.
How to Interpret this Plot:
X-Axis (Precipitation): Represents the total cumulative rain since January 1 of that year.
Y-Axis (Drought DSCI): Represents the total cumulative weekly USDM DSCI since January 1 of that year.
Watermarks (DRY / WET):
Upper-Left Corner (“DRY”): High cumulative drought index but low rainfall. Curves that bend steeply upwards and stay to the left represent persistent, severe drought years.
Lower-Right Corner (“WET”): High cumulative rainfall but low/no drought index. Curves that stretch far to the right and remain low represent wet, drought-free years.
Month Circles: Progress from left to right chronologically (January to December), allowing you to trace the exact weeks when drought conditions escalated (steep slopes) or recovered (slopes flattening out as rainfall accumulated).
---title: "Visualizing Climate Records using Plotly"subtitle: "10-Year Interactive Drought and Precipitation Analysis for South Dakota, Pine Ridge, and Rosebud"author: "Antigravity Coding Assistant"date: "2026-07-19"format: html: theme: cosmo toc: true code-fold: true code-tools: true embed-resources: true---# IntroductionIn this document, we visualize the weekly drought records for Oglala Lakota County and Todd County alongside the daily precipitation records for South Dakota using Python's **[Plotly](https://plotly.com/python/)** library, providing interactive and responsive dashboards.These counties correspond to part of the Pine Ridge and Rosebud reservationsand are labeled as such in plots.We will display:1. Both county drought datasets and the statewide rolling precipitation plotted over time (10-year timeline).2. Time within the year (standardized month, X-axis) vs. annual cumulative values (Y-axis) for cumulative precipitation and the two counties' cumulative drought indices.3. Annual trajectories comparing cumulative precipitation (X-axis) vs. cumulative drought index (Y-axis) faceted side-by-side by county.---# Data Processing & AlignmentFirst, we load the collected datasets, calculate the **Drought Severity and Coverage Index (DSCI)**, compute **cumulative precipitation**, and merge the tables on matching dates.- **DSCI Formula**: For cumulative USDM statistics, the index is calculated as `DSCI = D0 + D1 + D2 + D3 + D4` (range: 0 to 500).- **Date Matching**: We align the daily precipitation and weekly drought records on the weekly `ValidStart` date of each USDM report.```{python}#| label: data-preparation#| warning: false#| message: falseimport pandas as pdimport numpy as np# Load local CSV filesdf_usdm = pd.read_csv("data/oglala_lakota_drought_weekly.csv")df_todd = pd.read_csv("data/todd_drought_weekly.csv")df_precip = pd.read_csv("data/south_dakota_precipitation_daily.csv")# Parse datesdf_usdm["date"] = pd.to_datetime(df_usdm["ValidStart"])df_todd["date"] = pd.to_datetime(df_todd["ValidStart"])df_precip["date"] = pd.to_datetime(df_precip["date"])# --- Pine Ridge Preprocessing ---df_usdm["dsci"] = df_usdm["D0"] + df_usdm["D1"] + df_usdm["D2"] + df_usdm["D3"] + df_usdm["D4"]df_usdm["year"] = df_usdm["date"].dt.yeardf_usdm = df_usdm.sort_values("date")df_usdm["cumulative_dsci_year"] = df_usdm.groupby("year")["dsci"].cumsum()# --- Rosebud Preprocessing ---df_todd["dsci"] = df_todd["D0"] + df_todd["D1"] + df_todd["D2"] + df_todd["D3"] + df_todd["D4"]df_todd["year"] = df_todd["date"].dt.yeardf_todd = df_todd.sort_values("date")df_todd["cumulative_dsci_year"] = df_todd.groupby("year")["dsci"].cumsum()# --- South Dakota Precipitation Preprocessing ---df_precip = df_precip.sort_values("date").reset_index(drop=True)df_precip["cumulative_rain"] = df_precip["precipitation_inches"].cumsum()df_precip["year"] = df_precip["date"].dt.yeardf_precip["cumulative_rain_year"] = df_precip.groupby("year")["precipitation_inches"].cumsum()df_precip["rolling_365d_rain"] = df_precip["precipitation_inches"].rolling(window=365, min_periods=1).sum()# --- Merges ---df_merged_oglala = pd.merge( df_usdm[["date", "year", "dsci", "cumulative_dsci_year"]], df_precip[["date", "precipitation_inches", "cumulative_rain_year", "rolling_365d_rain"]], on="date", how="inner").sort_values("date").reset_index(drop=True)df_merged_oglala["year_str"] = df_merged_oglala["year"].astype(str)df_merged_oglala["day_in_year"] = pd.to_datetime(df_merged_oglala["date"].dt.strftime("2020-%m-%d"))df_merged_todd = pd.merge( df_todd[["date", "year", "dsci", "cumulative_dsci_year"]], df_precip[["date", "precipitation_inches", "cumulative_rain_year", "rolling_365d_rain"]], on="date", how="inner").sort_values("date").reset_index(drop=True)df_merged_todd["year_str"] = df_merged_todd["year"].astype(str)df_merged_todd["day_in_year"] = pd.to_datetime(df_merged_todd["date"].dt.strftime("2020-%m-%d"))df_merged_oglala.head()```---# Interactive Visualizations with Plotly## 1. Visualizing Data Over Time (10-Year Timelines)This faceted timeline displays the 10-year history of the weekly drought index (DSCI) for both counties alongside the daily South Dakota precipitation rolling average.```{python}#| label: plot-time-series#| warning: false#| message: falsefrom plotly.subplots import make_subplotsimport plotly.graph_objects as gofig_time = make_subplots( rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.06, subplot_titles=("DSCI - Pine Ridge", "DSCI - Rosebud", "365-Day Rolling Precipitation (inches) [South Dakota Mean]" ))# Pine Ridgefig_time.add_trace( go.Scatter( x=df_merged_oglala["date"], y=df_merged_oglala["dsci"], name="Pine Ridge DSCI", line=dict(color="#1f77b4", width=1.5) ), row=1, col=1)# Rosebudfig_time.add_trace( go.Scatter( x=df_merged_todd["date"], y=df_merged_todd["dsci"], name="Rosebud DSCI", line=dict(color="#ff7f0e", width=1.5) ), row=2, col=1)# South Dakota rolling precipitationfig_time.add_trace( go.Scatter( x=df_merged_oglala["date"], y=df_merged_oglala["rolling_365d_rain"], name="365-Day Rolling Precip", line=dict(color="#2ca02c", width=1.5) ), row=3, col=1)fig_time.update_layout( height=650, template="plotly_white", title=dict( text="10-Year Climate Metrics Over Time", y=0.98, x=0.5, xanchor="center", yanchor="top" ), margin=dict(t=50), showlegend=False, updatemenus=[dict(type="buttons", direction="right", x=1, y=0, xanchor="left", yanchor="bottom", showactive=False, buttons=[dict( label="Reset View", method="relayout", args=[{"xaxis.autorange": True,"yaxis.autorange": True,"yaxis2.autorange": True,"yaxis3.autorange": True }] ) ] ) ])fig_time.show()```---## 2. Visualizing Annual Cumulative Progression Over the Year (Time vs. Cumulative Values)Here we plot time within the year (standardized month, X-axis) against cumulative values (Y-axis). This contains 3 panels:- **Cumulative Precipitation (South Dakota Mean)**- **Cumulative Drought Index (Pine Ridge)**- **Cumulative Drought Index (Rosebud)**Each year is shown as a separate colored curve in a rainbow series, with open circles indicating the end of each month. Click on a year in the legend to toggle its visibility across all subplots.```{python}#| label: plot-time-vs-cumulative#| warning: false#| message: falseimport plotly.express as px# Identify colors for each year in a rainbow series (turbo colorscale)years =sorted(df_merged_oglala["year_str"].unique())colors = px.colors.sample_colorscale("turbo", [i / (len(years) -1) for i inrange(len(years))])year_colors =dict(zip(years, colors))fig_cum_time = make_subplots( rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.06, subplot_titles=("Cumulative Precipitation (South Dakota Mean)", "Cumulative Drought Index (Pine Ridge)", "Cumulative Drought Index (Rosebud)" ))# Identify month-end markersdf_month_ends_oglala = df_merged_oglala.groupby(["year", df_merged_oglala["date"].dt.month]).tail(1).copy()df_month_ends_oglala["day_in_year"] = pd.to_datetime(df_month_ends_oglala["date"].dt.strftime("2020-%m-%d"))df_month_ends_todd = df_merged_todd.groupby(["year", df_merged_todd["date"].dt.month]).tail(1).copy()df_month_ends_todd["day_in_year"] = pd.to_datetime(df_month_ends_todd["date"].dt.strftime("2020-%m-%d"))for year in years:# 1. Cumulative Precipitation df_y_p = df_merged_oglala[df_merged_oglala["year_str"] == year] df_pts_p = df_month_ends_oglala[df_month_ends_oglala["year_str"] == year] fig_cum_time.add_trace( go.Scatter( x=df_y_p["day_in_year"], y=df_y_p["cumulative_rain_year"], mode="lines", line=dict(color=year_colors[year]), name=year, legendgroup=year, showlegend=True ), row=1, col=1 ) fig_cum_time.add_trace( go.Scatter( x=df_pts_p["day_in_year"], y=df_pts_p["cumulative_rain_year"], mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), name=year, legendgroup=year, showlegend=False ), row=1, col=1 )# 2. Pine Ridge DSCI df_y_o = df_merged_oglala[df_merged_oglala["year_str"] == year] df_pts_o = df_month_ends_oglala[df_month_ends_oglala["year_str"] == year] fig_cum_time.add_trace( go.Scatter( x=df_y_o["day_in_year"], y=df_y_o["cumulative_dsci_year"], mode="lines", line=dict(color=year_colors[year]), name=year, legendgroup=year, showlegend=False ), row=2, col=1 ) fig_cum_time.add_trace( go.Scatter( x=df_pts_o["day_in_year"], y=df_pts_o["cumulative_dsci_year"], mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), name=year, legendgroup=year, showlegend=False ), row=2, col=1 )# 3. Rosebud DSCI df_y_t = df_merged_todd[df_merged_todd["year_str"] == year] df_pts_t = df_month_ends_todd[df_month_ends_todd["year_str"] == year] fig_cum_time.add_trace( go.Scatter( x=df_y_t["day_in_year"], y=df_y_t["cumulative_dsci_year"], mode="lines", line=dict(color=year_colors[year]), name=year, legendgroup=year, showlegend=False ), row=3, col=1 ) fig_cum_time.add_trace( go.Scatter( x=df_pts_t["day_in_year"], y=df_pts_t["cumulative_dsci_year"], mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), name=year, legendgroup=year, showlegend=False ), row=3, col=1 )fig_cum_time.update_layout( height =880, template ="plotly_white", title =dict( text ="Annual Progression of Cumulative Climate Metrics", y =0.95, x =0.5, xanchor ="center", yanchor ="top" ), margin =dict(t =90), legend =dict(# groupclick="togglegroup", <-- You can replace or keep this, but the below are safer for multi-subplots# itemclick = "toggle3", # Single-click toggles the entire group# itemdoubleclick = "isolate3", # Double-click isolates the entire group groupclick="togglegroup", # Natively validated to handle groups on double-click title_text ="Year" ), updatemenus=[dict(type="buttons", direction="right", x=1, y=0, xanchor="left", yanchor="bottom", showactive=False, buttons=[dict( label="Reset View", method="update", args=[ {"visible": [True] *len(fig_cum_time.data)}, {"xaxis.autorange": True,"yaxis.autorange": True,"yaxis2.autorange": True,"yaxis3.autorange": True } ] ) ] ) ])# Format the x-axesfig_cum_time.update_xaxes(tickformat="%b", dtick="M1")fig_cum_time.show()```---## 3. Visualizing Trajectories of Cumulative Rain vs. Cumulative Drought Index (Faceted by County)Finally, we plot **annual cumulative precipitation** (X-axis) against the **annual cumulative drought index (DSCI)** (Y-axis). The curves for Pine Ridge and Rosebud are displayed side-by-side as two facets on the same plot surface. Individual years are colored in a rainbow series with open circles marking the end of each month.### How to Interpret this Plot:- **X-Axis (Precipitation)**: Represents the total cumulative rain since January 1 of that year.- **Y-Axis (Drought DSCI)**: Represents the total cumulative weekly USDM DSCI since January 1 of that year.- **Watermarks (DRY / WET)**: - **Upper-Left Corner ("DRY")**: High cumulative drought index but low rainfall. Curves that bend steeply upwards and stay to the left represent persistent, severe drought years. - **Lower-Right Corner ("WET")**: High cumulative rainfall but low/no drought index. Curves that stretch far to the right and remain low represent wet, drought-free years. - **Month Circles**: Progress from left to right chronologically (January to December), allowing you to trace the exact weeks when drought conditions escalated (steep slopes) or recovered (slopes flattening out as rainfall accumulated).```{python}#| label: plot-combined-annual-trajectories#| warning: false#| message: falsefig_combined_annual = make_subplots( rows=1, cols=2, shared_yaxes=True, horizontal_spacing=0.08, subplot_titles=("Pine Ridge", "Rosebud"))# Identify month-end markers for Rosebuddf_month_ends_todd = df_merged_todd.groupby(["year", df_merged_todd["date"].dt.month]).tail(1).copy()for year in years:# 1. Pine Ridge df_y_o = df_merged_oglala[df_merged_oglala["year_str"] == year] df_pts_o = df_month_ends_oglala[df_month_ends_oglala["year_str"] == year] fig_combined_annual.add_trace( go.Scatter( x=df_y_o["cumulative_rain_year"], y=df_y_o["cumulative_dsci_year"], mode="lines", line=dict(color=year_colors[year]), name=year, legendgroup=year, showlegend=True ), row=1, col=1 ) fig_combined_annual.add_trace( go.Scatter( x=df_pts_o["cumulative_rain_year"], y=df_pts_o["cumulative_dsci_year"], mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), name=year, legendgroup=year, showlegend=False ), row=1, col=1 )# 2. Rosebud df_y_t = df_merged_todd[df_merged_todd["year_str"] == year] df_pts_t = df_month_ends_todd[df_month_ends_todd["year_str"] == year] fig_combined_annual.add_trace( go.Scatter( x=df_y_t["cumulative_rain_year"], y=df_y_t["cumulative_dsci_year"], mode="lines", line=dict(color=year_colors[year]), name=year, legendgroup=year, showlegend=False ), row=1, col=2 ) fig_combined_annual.add_trace( go.Scatter( x=df_pts_t["cumulative_rain_year"], y=df_pts_t["cumulative_dsci_year"], mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), name=year, legendgroup=year, showlegend=False ), row=1, col=2 )# Add background watermark text annotations for DRY and WET zones# Subplot 1 (Pine Ridge)fig_combined_annual.add_annotation( xref="x domain", yref="y domain", x=0.15, y=0.85, text="<b>DRY</b>", showarrow=False, font=dict(size=40, color="rgba(150, 150, 150, 0.2)"), textangle=-30)fig_combined_annual.add_annotation( xref="x domain", yref="y domain", x=0.85, y=0.15, text="<b>WET</b>", showarrow=False, font=dict(size=40, color="rgba(150, 150, 150, 0.2)"), textangle=-30)# Subplot 2 (Rosebud)fig_combined_annual.add_annotation( xref="x2 domain", yref="y2 domain", x=0.15, y=0.85, text="<b>DRY</b>", showarrow=False, font=dict(size=40, color="rgba(150, 150, 150, 0.2)"), textangle=-30)fig_combined_annual.add_annotation( xref="x2 domain", yref="y2 domain", x=0.85, y=0.15, text="<b>WET</b>", showarrow=False, font=dict(size=40, color="rgba(150, 150, 150, 0.2)"), textangle=-30)fig_combined_annual.update_layout( height=480, template="plotly_white", title=dict( text="Annual Cumulative Rain vs. Cumulative Drought Index (DSCI) Trajectories", y=0.94, x=0.5, xanchor="center", yanchor="top" ), margin=dict(t=65), legend =dict(# groupclick="togglegroup", <-- You can replace or keep this, but the below are safer for multi-subplots# itemclick = "toggle3", # Single-click toggles the entire group# itemdoubleclick = "isolate3", # Double-click isolates the entire group groupclick="togglegroup", # Natively validated to handle groups on double-click title_text ="Year" ), updatemenus=[dict(type="buttons", direction="right", x=1, y=0, xanchor="left", yanchor="bottom", showactive=False, buttons=[dict( label="Reset View", method="update", args=[ {"visible": [True] *len(fig_combined_annual.data)}, {"xaxis.autorange": True,"yaxis.autorange": True,"xaxis2.autorange": True,"yaxis2.autorange": True } ] ) ] ) ])# Format the x-axesfig_combined_annual.update_xaxes(title_text="Cumulative Precipitation (inches)")fig_combined_annual.update_yaxes(title_text="Cumulative DSCI", row=1, col=1)fig_combined_annual.show()```