# Import required libraries
import pandas as pd
import numpy as np
from pathlib import Path
import matplotlib.pyplot as plt
import seaborn as sns
from great_tables import GT, style, loc
# Configuration
= "../../separations_accessions/parquet/"
PARQUET_DIR = "AG11"
FOREST_SERVICE_CODE
# Define color scales for heatmaps
= "#1f77b4" # Blue for accessions
ACCESSIONS_COLOR = "#ff7f0e" # Orange for separations
SEPARATIONS_COLOR = "#2ca02c" # Green for positive net
NET_POSITIVE_COLOR = "#d62728" # Red for negative net NET_NEGATIVE_COLOR
Forest Service Accessions and Separations Analysis
Data Loading
Show code
# Load accessions data
= [
accessions_files "fedscope_accessions_FY2005-2009.parquet",
"fedscope_accessions_FY2010-2014.parquet",
"fedscope_accessions_FY2015-2019.parquet",
"fedscope_accessions_FY2020-2024.parquet",
"fedscope_accessions_March_2025.parquet"
]
# Load separations data
= [
separations_files "fedscope_separations_FY2005-2009.parquet",
"fedscope_separations_FY2010-2014.parquet",
"fedscope_separations_FY2015-2019.parquet",
"fedscope_separations_FY2020-2024.parquet",
"fedscope_separations_March_2025.parquet"
]
# Load all accessions data
= []
accessions_data for file in accessions_files:
try:
= pd.read_parquet(PARQUET_DIR + file)
df # Handle both uppercase and lowercase column names
= 'AGYSUB' if 'AGYSUB' in df.columns else 'agysub'
agysub_col # Filter for Forest Service
= df[df[agysub_col] == FOREST_SERVICE_CODE].copy()
df_forest if not df_forest.empty:
accessions_data.append(df_forest)print(f"Loaded {len(df_forest)} Forest Service accessions records from {file}")
except Exception as e:
print(f"Error loading {file}: {e}")
# Load all separations data
= []
separations_data for file in separations_files:
try:
= pd.read_parquet(PARQUET_DIR + file)
df # Filter for Forest Service
= df[df['agysub'] == FOREST_SERVICE_CODE].copy()
df_forest if not df_forest.empty:
separations_data.append(df_forest)print(f"Loaded {len(df_forest)} Forest Service separations records from {file}")
except Exception as e:
print(f"Error loading {file}: {e}")
# Combine all data
= pd.concat(accessions_data, ignore_index=True) if accessions_data else pd.DataFrame()
all_accessions = pd.concat(separations_data, ignore_index=True) if separations_data else pd.DataFrame()
all_separations
print(f"\nTotal Forest Service accessions records: {len(all_accessions):,}")
print(f"Total Forest Service separations records: {len(all_separations):,}")
Loaded 74461 Forest Service accessions records from fedscope_accessions_FY2005-2009.parquet
Loaded 69871 Forest Service accessions records from fedscope_accessions_FY2010-2014.parquet
Loaded 67422 Forest Service accessions records from fedscope_accessions_FY2015-2019.parquet
Loaded 49603 Forest Service accessions records from fedscope_accessions_FY2020-2024.parquet
Loaded 8031 Forest Service accessions records from fedscope_accessions_March_2025.parquet
Loaded 77754 Forest Service separations records from fedscope_separations_FY2005-2009.parquet
Loaded 71494 Forest Service separations records from fedscope_separations_FY2010-2014.parquet
Loaded 68659 Forest Service separations records from fedscope_separations_FY2015-2019.parquet
Loaded 45472 Forest Service separations records from fedscope_separations_FY2020-2024.parquet
Loaded 8249 Forest Service separations records from fedscope_separations_March_2025.parquet
Total Forest Service accessions records: 269,388
Total Forest Service separations records: 271,628
Monthly Aggregation
Show code
def aggregate_by_month(df, data_type):
"""Aggregate data by month (EFDATE field)."""
if df.empty:
return pd.DataFrame()
# Convert EFDATE to string and extract year/month
'efdate_str'] = df['efdate'].astype(str)
df['year'] = df['efdate_str'].str[:4].astype(int)
df['month'] = df['efdate_str'].str[4:6].astype(int)
df[
# Group by year and month
= df.groupby(['year', 'month']).size().reset_index(name='count')
monthly 'data_type'] = data_type
monthly[
return monthly
# Aggregate accessions and separations by month
= aggregate_by_month(all_accessions, 'accessions')
monthly_accessions = aggregate_by_month(all_separations, 'separations')
monthly_separations
# Merge to calculate net change
if not monthly_accessions.empty and not monthly_separations.empty:
= pd.merge(
monthly_combined 'year', 'month', 'count']],
monthly_accessions[['year', 'month', 'count']],
monthly_separations[[=['year', 'month'],
on='outer',
how=('_acc', '_sep')
suffixes0)
).fillna(
'accessions'] = monthly_combined['count_acc'].astype(int)
monthly_combined['separations'] = monthly_combined['count_sep'].astype(int)
monthly_combined['net_change'] = monthly_combined['accessions'] - monthly_combined['separations']
monthly_combined[= monthly_combined.drop(['count_acc', 'count_sep'], axis=1)
monthly_combined else:
= pd.DataFrame()
monthly_combined
print(f"Monthly data points: {len(monthly_combined)}")
Monthly data points: 240
Accessions Heatmap
Show code
if not monthly_combined.empty:
# Create pivot table for accessions
= monthly_combined.pivot(index='month', columns='year', values='accessions')
accessions_pivot
# Get most recent 10 years for main display
= sorted(accessions_pivot.columns)
all_years = all_years[-10:] # Last 10 years
recent_years = accessions_pivot[recent_years]
recent_accessions
# Create month labels
= ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
month_labels = [month_labels[i-1] for i in recent_accessions.index]
recent_accessions.index
# Create GT heatmap for accessions (recent years)
= recent_accessions.reset_index()
accessions_df = ['Month'] + [str(col) for col in recent_accessions.columns]
accessions_df.columns
= (
gt_accessions
GT(accessions_df)=f"Forest Service Monthly Accessions ({recent_years[0]}-{recent_years[-1]})")
.tab_header(title=list(accessions_df.columns[1:]), decimals=0, use_seps=True)
.fmt_number(columns
.data_color(=list(accessions_df.columns[1:]),
columns=["white", ACCESSIONS_COLOR],
palette=[0, accessions_pivot.max().max()],
domain="lightgray"
na_color
)
)
gt_accessions.show()
Forest Service Monthly Accessions (2016-2025) | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
Month | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 |
Jan | 350 | 784 | 238 | 83 | 272 | 351 | 285 | 631 | 123 | |
Feb | 300 | 193 | 247 | 233 | 293 | 309 | 441 | 436 | 111 | |
Mar | 738 | 422 | 556 | 944 | 1,377 | 911 | 1,062 | 1,125 | 502 | |
Apr | 2,163 | 4,480 | 4,088 | 3,720 | 2,644 | 3,035 | 2,984 | 2,884 | 1,949 | |
May | 7,735 | 5,245 | 5,373 | 4,987 | 5,556 | 4,566 | 3,605 | 3,225 | 2,910 | |
Jun | 1,336 | 1,354 | 1,252 | 1,406 | 1,450 | 1,144 | 1,408 | 2,046 | 1,744 | |
Jul | 488 | 371 | 398 | 341 | 402 | 288 | 899 | 788 | 221 | |
Aug | 271 | 235 | 317 | 331 | 503 | 260 | 403 | 415 | 151 | |
Sep | 198 | 99 | 269 | 251 | 234 | 229 | 304 | 383 | 82 | |
Oct | 300 | 151 | 156 | 234 | 215 | 213 | 393 | 102 | ||
Nov | 139 | 107 | 156 | 178 | 143 | 154 | 267 | 82 | ||
Dec | 186 | 97 | 88 | 153 | 143 | 116 | 246 | 54 |
Separations Heatmap
Show code
if not monthly_combined.empty:
# Create pivot table for separations
= monthly_combined.pivot(index='month', columns='year', values='separations')
separations_pivot
# Get most recent 10 years for main display
= separations_pivot[recent_years]
recent_separations
# Create month labels
= [month_labels[i-1] for i in recent_separations.index]
recent_separations.index
# Create GT heatmap for separations (recent years)
= recent_separations.reset_index()
separations_df = ['Month'] + [str(col) for col in recent_separations.columns]
separations_df.columns
= (
gt_separations
GT(separations_df)=f"Forest Service Monthly Separations ({recent_years[0]}-{recent_years[-1]})")
.tab_header(title=list(separations_df.columns[1:]), decimals=0, use_seps=True)
.fmt_number(columns
.data_color(=list(separations_df.columns[1:]),
columns=["white", SEPARATIONS_COLOR],
palette=[0, separations_pivot.max().max()],
domain="lightgray"
na_color
)
)
gt_separations.show()
Forest Service Monthly Separations (2016-2025) | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
Month | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 |
Jan | 517 | 533 | 449 | 382 | 454 | 504 | 374 | 341 | 378 | |
Feb | 271 | 258 | 264 | 367 | 326 | 245 | 351 | 337 | 279 | |
Mar | 305 | 314 | 470 | 424 | 299 | 350 | 378 | 401 | 434 | |
Apr | 455 | 386 | 394 | 354 | 306 | 412 | 414 | 362 | 439 | |
May | 369 | 366 | 372 | 369 | 290 | 376 | 399 | 288 | 372 | |
Jun | 366 | 343 | 364 | 332 | 310 | 357 | 340 | 375 | 352 | |
Jul | 385 | 391 | 345 | 374 | 440 | 602 | 509 | 381 | 369 | |
Aug | 1,622 | 1,550 | 1,512 | 1,539 | 1,359 | 1,362 | 1,203 | 1,255 | 1,043 | |
Sep | 2,153 | 2,648 | 2,446 | 2,278 | 1,623 | 1,703 | 1,558 | 1,248 | 1,086 | |
Oct | 4,403 | 3,836 | 3,554 | 3,169 | 2,647 | 2,867 | 2,147 | 1,744 | ||
Nov | 2,267 | 2,449 | 2,394 | 2,341 | 2,449 | 2,171 | 1,746 | 1,219 | ||
Dec | 902 | 772 | 785 | 1,005 | 1,016 | 1,079 | 1,003 | 534 |
Net Change Heatmap
Show code
if not monthly_combined.empty:
# Create pivot table for net change
= monthly_combined.pivot(index='month', columns='year', values='net_change')
net_pivot
# Get most recent 10 years for main display
= net_pivot[recent_years]
recent_net
# Create month labels
= [month_labels[i-1] for i in recent_net.index]
recent_net.index
# Get max absolute value for symmetric scale (from all data)
= max(abs(net_pivot.min().min()), abs(net_pivot.max().max()))
max_abs_val
# Create GT heatmap for net change with diverging colors (recent years)
= recent_net.reset_index()
net_df = ['Month'] + [str(col) for col in recent_net.columns]
net_df.columns
= (
gt_net
GT(net_df)=f"Forest Service Monthly Net Change ({recent_years[0]}-{recent_years[-1]})")
.tab_header(title=list(net_df.columns[1:]), decimals=0, use_seps=True)
.fmt_number(columns
.data_color(=list(net_df.columns[1:]),
columns=[NET_NEGATIVE_COLOR, "white", NET_POSITIVE_COLOR],
palette=[-max_abs_val, max_abs_val],
domain="lightgray"
na_color
)
)
gt_net.show()
Forest Service Monthly Net Change (2016-2025) | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
Month | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 |
Jan | −167 | 251 | −211 | −299 | −182 | −153 | −89 | 290 | −255 | |
Feb | 29 | −65 | −17 | −134 | −33 | 64 | 90 | 99 | −168 | |
Mar | 433 | 108 | 86 | 520 | 1,078 | 561 | 684 | 724 | 68 | |
Apr | 1,708 | 4,094 | 3,694 | 3,366 | 2,338 | 2,623 | 2,570 | 2,522 | 1,510 | |
May | 7,366 | 4,879 | 5,001 | 4,618 | 5,266 | 4,190 | 3,206 | 2,937 | 2,538 | |
Jun | 970 | 1,011 | 888 | 1,074 | 1,140 | 787 | 1,068 | 1,671 | 1,392 | |
Jul | 103 | −20 | 53 | −33 | −38 | −314 | 390 | 407 | −148 | |
Aug | −1,351 | −1,315 | −1,195 | −1,208 | −856 | −1,102 | −800 | −840 | −892 | |
Sep | −1,955 | −2,549 | −2,177 | −2,027 | −1,389 | −1,474 | −1,254 | −865 | −1,004 | |
Oct | −4,103 | −3,685 | −3,398 | −2,935 | −2,432 | −2,654 | −1,754 | −1,642 | ||
Nov | −2,128 | −2,342 | −2,238 | −2,163 | −2,306 | −2,017 | −1,479 | −1,137 | ||
Dec | −716 | −675 | −697 | −852 | −873 | −963 | −757 | −480 |
Annual Trends (Complete Years Only)
Show code
if not monthly_combined.empty:
# Calculate annual totals and months per year
= monthly_combined.groupby('year').agg({
annual_summary 'accessions': 'sum',
'separations': 'sum',
'net_change': 'sum',
'month': 'count' # Count of months with data
}).reset_index()
# Only include years with 12 months of data
= annual_summary[annual_summary['month'] == 12].copy()
complete_years
if not complete_years.empty:
print(f"Showing {len(complete_years)} complete years with 12 months of data each")
print(f"Complete years: {sorted(complete_years['year'].tolist())}")
# Create line plot
= plt.subplots(figsize=(12, 6))
fig, ax
'year'], complete_years['accessions'],
ax.plot(complete_years[=ACCESSIONS_COLOR, linewidth=3, marker='o', markersize=8, label='Accessions')
color'year'], complete_years['separations'],
ax.plot(complete_years[=SEPARATIONS_COLOR, linewidth=3, marker='s', markersize=8, label='Separations')
color'year'], complete_years['net_change'],
ax.plot(complete_years[='black', linewidth=2, marker='D', markersize=6, label='Net Change', linestyle='--')
color
# Add zero line
=0, color='gray', linestyle='-', alpha=0.5)
ax.axhline(y
# Formatting
'Year', fontsize=12)
ax.set_xlabel('Count', fontsize=12)
ax.set_ylabel('Forest Service Annual Accessions, Separations, and Net Change (Complete Years Only)', fontsize=14, fontweight='bold')
ax.set_title(True, alpha=0.3)
ax.grid(
ax.legend()
# Format y-axis with commas and ensure integer x-axis
lambda x, p: f'{int(x):,}'))
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'{int(x)}'))
ax.xaxis.set_major_formatter(plt.FuncFormatter(
plt.tight_layout()
plt.show()else:
print("No complete years (12 months of data) found for annual analysis")
# Show what we have
print("\nYears with partial data:")
for _, row in annual_summary.iterrows():
print(f" {row['year']}: {row['month']} months of data")
Showing 18 complete years with 12 months of data each
Complete years: [2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022]
Complete Historical View (All Years)
Show code
if not monthly_combined.empty:
# Full historical pivot tables
= monthly_combined.pivot(index='month', columns='year', values='accessions')
accessions_pivot_full = monthly_combined.pivot(index='month', columns='year', values='separations')
separations_pivot_full = monthly_combined.pivot(index='month', columns='year', values='net_change')
net_pivot_full
# Apply month labels to all
for pivot in [accessions_pivot_full, separations_pivot_full, net_pivot_full]:
= [month_labels[i-1] for i in pivot.index]
pivot.index
# Create tiny accessions table
= accessions_pivot_full.reset_index()
acc_df_full = ['Month'] + [str(col) for col in accessions_pivot_full.columns]
acc_df_full.columns
= (
gt_acc_full
GT(acc_df_full)="Forest Service Monthly Accessions (All Years)")
.tab_header(title=list(acc_df_full.columns[1:]), decimals=0, use_seps=True)
.fmt_number(columns
.data_color(=list(acc_df_full.columns[1:]),
columns=["white", ACCESSIONS_COLOR],
palette=[0, accessions_pivot_full.max().max()],
domain="lightgray"
na_color
)
.tab_options(="6px",
table_font_size="1px",
data_row_padding="5px"
column_labels_font_size
)
)
gt_acc_full.show()
# Create tiny separations table
= separations_pivot_full.reset_index()
sep_df_full = ['Month'] + [str(col) for col in separations_pivot_full.columns]
sep_df_full.columns
= (
gt_sep_full
GT(sep_df_full)="Forest Service Monthly Separations (All Years)")
.tab_header(title=list(sep_df_full.columns[1:]), decimals=0, use_seps=True)
.fmt_number(columns
.data_color(=list(sep_df_full.columns[1:]),
columns=["white", SEPARATIONS_COLOR],
palette=[0, separations_pivot_full.max().max()],
domain="lightgray"
na_color
)
.tab_options(="6px",
table_font_size="1px",
data_row_padding="5px"
column_labels_font_size
)
)
gt_sep_full.show()
# Create tiny net change table
= net_pivot_full.reset_index()
net_df_full = ['Month'] + [str(col) for col in net_pivot_full.columns]
net_df_full.columns
= max(abs(net_pivot_full.min().min()), abs(net_pivot_full.max().max()))
max_abs_val_full
= (
gt_net_full
GT(net_df_full)="Forest Service Monthly Net Change (All Years)")
.tab_header(title=list(net_df_full.columns[1:]), decimals=0, use_seps=True)
.fmt_number(columns
.data_color(=list(net_df_full.columns[1:]),
columns=[NET_NEGATIVE_COLOR, "white", NET_POSITIVE_COLOR],
palette=[-max_abs_val_full, max_abs_val_full],
domain="lightgray"
na_color
)
.tab_options(="6px",
table_font_size="1px",
data_row_padding="5px"
column_labels_font_size
)
) gt_net_full.show()
Forest Service Monthly Accessions (All Years) | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Month | 2004 | 2005 | 2006 | 2007 | 2008 | 2009 | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 |
Jan | 411 | 392 | 266 | 217 | 323 | 909 | 525 | 246 | 86 | 220 | 393 | 350 | 784 | 238 | 83 | 272 | 351 | 285 | 631 | 123 | ||
Feb | 348 | 482 | 284 | 129 | 276 | 594 | 452 | 206 | 142 | 188 | 257 | 300 | 193 | 247 | 233 | 293 | 309 | 441 | 436 | 111 | ||
Mar | 589 | 616 | 429 | 914 | 1,186 | 1,002 | 712 | 388 | 439 | 610 | 805 | 738 | 422 | 556 | 944 | 1,377 | 911 | 1,062 | 1,125 | 502 | ||
Apr | 1,545 | 3,046 | 3,858 | 2,563 | 2,748 | 2,672 | 1,863 | 1,859 | 1,720 | 1,757 | 2,008 | 2,163 | 4,480 | 4,088 | 3,720 | 2,644 | 3,035 | 2,984 | 2,884 | 1,949 | ||
May | 7,855 | 6,066 | 4,431 | 6,349 | 7,043 | 7,014 | 6,070 | 6,104 | 5,400 | 6,209 | 7,557 | 7,735 | 5,245 | 5,373 | 4,987 | 5,556 | 4,566 | 3,605 | 3,225 | 2,910 | ||
Jun | 2,772 | 2,679 | 2,014 | 2,988 | 3,185 | 3,545 | 3,140 | 2,954 | 2,926 | 3,003 | 1,292 | 1,336 | 1,354 | 1,252 | 1,406 | 1,450 | 1,144 | 1,408 | 2,046 | 1,744 | ||
Jul | 482 | 497 | 368 | 551 | 635 | 568 | 645 | 565 | 348 | 286 | 341 | 488 | 371 | 398 | 341 | 402 | 288 | 899 | 788 | 221 | ||
Aug | 353 | 297 | 223 | 440 | 489 | 506 | 186 | 188 | 174 | 206 | 265 | 271 | 235 | 317 | 331 | 503 | 260 | 403 | 415 | 151 | ||
Sep | 215 | 290 | 273 | 213 | 252 | 215 | 115 | 144 | 139 | 145 | 216 | 198 | 99 | 269 | 251 | 234 | 229 | 304 | 383 | 82 | ||
Oct | 330 | 317 | 262 | 230 | 186 | 226 | 260 | 103 | 263 | 119 | 165 | 196 | 300 | 151 | 156 | 234 | 215 | 213 | 393 | 102 | ||
Nov | 204 | 160 | 168 | 163 | 189 | 233 | 241 | 119 | 119 | 83 | 231 | 248 | 139 | 107 | 156 | 178 | 143 | 154 | 267 | 82 | ||
Dec | 170 | 140 | 129 | 79 | 152 | 224 | 178 | 70 | 84 | 164 | 125 | 147 | 186 | 97 | 88 | 153 | 143 | 116 | 246 | 54 |
Forest Service Monthly Separations (All Years) | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Month | 2004 | 2005 | 2006 | 2007 | 2008 | 2009 | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 |
Jan | 635 | 638 | 656 | 679 | 746 | 750 | 495 | 232 | 552 | 613 | 564 | 517 | 533 | 449 | 382 | 454 | 504 | 374 | 341 | 378 | ||
Feb | 352 | 298 | 273 | 317 | 263 | 293 | 255 | 205 | 248 | 230 | 221 | 271 | 258 | 264 | 367 | 326 | 245 | 351 | 337 | 279 | ||
Mar | 459 | 414 | 447 | 403 | 271 | 297 | 261 | 265 | 257 | 255 | 309 | 305 | 314 | 470 | 424 | 299 | 350 | 378 | 401 | 434 | ||
Apr | 604 | 463 | 456 | 339 | 272 | 316 | 309 | 283 | 256 | 316 | 380 | 455 | 386 | 394 | 354 | 306 | 412 | 414 | 362 | 439 | ||
May | 587 | 513 | 507 | 412 | 306 | 304 | 337 | 348 | 365 | 459 | 550 | 369 | 366 | 372 | 369 | 290 | 376 | 399 | 288 | 372 | ||
Jun | 574 | 531 | 535 | 380 | 277 | 360 | 324 | 400 | 371 | 305 | 317 | 366 | 343 | 364 | 332 | 310 | 357 | 340 | 375 | 352 | ||
Jul | 635 | 509 | 461 | 435 | 492 | 596 | 558 | 371 | 354 | 337 | 399 | 385 | 391 | 345 | 374 | 440 | 602 | 509 | 381 | 369 | ||
Aug | 3,186 | 3,276 | 2,873 | 3,657 | 3,523 | 3,395 | 2,539 | 2,472 | 1,566 | 1,645 | 1,509 | 1,622 | 1,550 | 1,512 | 1,539 | 1,359 | 1,362 | 1,203 | 1,255 | 1,043 | ||
Sep | 3,127 | 3,071 | 3,070 | 2,654 | 2,641 | 3,074 | 2,826 | 2,181 | 2,224 | 1,939 | 1,809 | 2,153 | 2,648 | 2,446 | 2,278 | 1,623 | 1,703 | 1,558 | 1,248 | 1,086 | ||
Oct | 4,436 | 3,787 | 2,719 | 2,507 | 2,578 | 3,672 | 3,929 | 3,702 | 3,161 | 3,192 | 3,505 | 4,291 | 4,403 | 3,836 | 3,554 | 3,169 | 2,647 | 2,867 | 2,147 | 1,744 | ||
Nov | 1,801 | 2,064 | 2,006 | 2,057 | 2,308 | 2,575 | 2,480 | 2,061 | 2,796 | 2,666 | 3,185 | 2,527 | 2,267 | 2,449 | 2,394 | 2,341 | 2,449 | 2,171 | 1,746 | 1,219 | ||
Dec | 1,143 | 798 | 741 | 797 | 795 | 944 | 1,102 | 1,122 | 1,130 | 624 | 709 | 755 | 902 | 772 | 785 | 1,005 | 1,016 | 1,079 | 1,003 | 534 |
Forest Service Monthly Net Change (All Years) | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Month | 2004 | 2005 | 2006 | 2007 | 2008 | 2009 | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 |
Jan | −224 | −246 | −390 | −462 | −423 | 159 | 30 | 14 | −466 | −393 | −171 | −167 | 251 | −211 | −299 | −182 | −153 | −89 | 290 | −255 | ||
Feb | −4 | 184 | 11 | −188 | 13 | 301 | 197 | 1 | −106 | −42 | 36 | 29 | −65 | −17 | −134 | −33 | 64 | 90 | 99 | −168 | ||
Mar | 130 | 202 | −18 | 511 | 915 | 705 | 451 | 123 | 182 | 355 | 496 | 433 | 108 | 86 | 520 | 1,078 | 561 | 684 | 724 | 68 | ||
Apr | 941 | 2,583 | 3,402 | 2,224 | 2,476 | 2,356 | 1,554 | 1,576 | 1,464 | 1,441 | 1,628 | 1,708 | 4,094 | 3,694 | 3,366 | 2,338 | 2,623 | 2,570 | 2,522 | 1,510 | ||
May | 7,268 | 5,553 | 3,924 | 5,937 | 6,737 | 6,710 | 5,733 | 5,756 | 5,035 | 5,750 | 7,007 | 7,366 | 4,879 | 5,001 | 4,618 | 5,266 | 4,190 | 3,206 | 2,937 | 2,538 | ||
Jun | 2,198 | 2,148 | 1,479 | 2,608 | 2,908 | 3,185 | 2,816 | 2,554 | 2,555 | 2,698 | 975 | 970 | 1,011 | 888 | 1,074 | 1,140 | 787 | 1,068 | 1,671 | 1,392 | ||
Jul | −153 | −12 | −93 | 116 | 143 | −28 | 87 | 194 | −6 | −51 | −58 | 103 | −20 | 53 | −33 | −38 | −314 | 390 | 407 | −148 | ||
Aug | −2,833 | −2,979 | −2,650 | −3,217 | −3,034 | −2,889 | −2,353 | −2,284 | −1,392 | −1,439 | −1,244 | −1,351 | −1,315 | −1,195 | −1,208 | −856 | −1,102 | −800 | −840 | −892 | ||
Sep | −2,912 | −2,781 | −2,797 | −2,441 | −2,389 | −2,859 | −2,711 | −2,037 | −2,085 | −1,794 | −1,593 | −1,955 | −2,549 | −2,177 | −2,027 | −1,389 | −1,474 | −1,254 | −865 | −1,004 | ||
Oct | −4,106 | −3,470 | −2,457 | −2,277 | −2,392 | −3,446 | −3,669 | −3,599 | −2,898 | −3,073 | −3,340 | −4,095 | −4,103 | −3,685 | −3,398 | −2,935 | −2,432 | −2,654 | −1,754 | −1,642 | ||
Nov | −1,597 | −1,904 | −1,838 | −1,894 | −2,119 | −2,342 | −2,239 | −1,942 | −2,677 | −2,583 | −2,954 | −2,279 | −2,128 | −2,342 | −2,238 | −2,163 | −2,306 | −2,017 | −1,479 | −1,137 | ||
Dec | −973 | −658 | −612 | −718 | −643 | −720 | −924 | −1,052 | −1,046 | −460 | −584 | −608 | −716 | −675 | −697 | −852 | −873 | −963 | −757 | −480 |
Key Insights
Show code
if not monthly_combined.empty:
# Find months with highest accessions/separations
= monthly_combined['accessions'].idxmax()
max_acc_idx = monthly_combined['separations'].idxmax()
max_sep_idx = monthly_combined[monthly_combined['net_change'] > 0]['net_change'].idxmax() if (monthly_combined['net_change'] > 0).any() else None
max_net_pos_idx = monthly_combined[monthly_combined['net_change'] < 0]['net_change'].idxmin() if (monthly_combined['net_change'] < 0).any() else None
max_net_neg_idx
= []
insights
if not pd.isna(max_acc_idx):
= monthly_combined.loc[max_acc_idx]
max_acc
insights.append({'Metric': 'Highest Monthly Accessions',
'Month': f"{month_labels[int(max_acc['month'])-1]} {int(max_acc['year'])}",
'Value': f"{int(max_acc['accessions']):,}"
})
if not pd.isna(max_sep_idx):
= monthly_combined.loc[max_sep_idx]
max_sep
insights.append({'Metric': 'Highest Monthly Separations',
'Month': f"{month_labels[int(max_sep['month'])-1]} {int(max_sep['year'])}",
'Value': f"{int(max_sep['separations']):,}"
})
if max_net_pos_idx is not None and not pd.isna(max_net_pos_idx):
= monthly_combined.loc[max_net_pos_idx]
max_net_pos
insights.append({'Metric': 'Largest Net Gain',
'Month': f"{month_labels[int(max_net_pos['month'])-1]} {int(max_net_pos['year'])}",
'Value': f"+{int(max_net_pos['net_change']):,}"
})
if max_net_neg_idx is not None and not pd.isna(max_net_neg_idx):
= monthly_combined.loc[max_net_neg_idx]
max_net_neg
insights.append({'Metric': 'Largest Net Loss',
'Month': f"{month_labels[int(max_net_neg['month'])-1]} {int(max_net_neg['year'])}",
'Value': f"{int(max_net_neg['net_change']):,}"
})
if insights:
= pd.DataFrame(insights)
insights_df
= (
gt_insights
GT(insights_df)="Key Insights")
.tab_header(title
)
gt_insights.show()
Key Insights | ||
---|---|---|
Metric | Month | Value |
Highest Monthly Accessions | May 2005 | 7,855 |
Highest Monthly Separations | Oct 2004 | 4,436 |
Largest Net Gain | May 2016 | +7,366 |
Largest Net Loss | Oct 2004 | -4,106 |