Using this notebook¶
Climate Trends in Ramona, California (1998–2025)¶
This notebook analyzes long-term temperature trends in Ramona, CA using NOAA NCEI data. We assess whether local temperature patterns support the assumption that Ramona has experienced measurable climate change over the past two decades.
How this supports the broader project¶
The increasing minimum and maximum temperatures in Ramona provide important context for interpreting vegetation recovery. Warmer conditions may influence drought stress, shrubland regeneration, and NDVI patterns in the years following major wildfire events.
Step 0: Libraries¶
- Import required libraries
Step 1: Import the climate data¶
- Pull the NCEI data
Step 2: Clean the dateframe¶
- For this portion of the project we need to pull the TMAX column
Step 3: Convert temperature units¶
- For this portion of the project we will relable and recalculate the temperatures under the TMAX and TMIN columns
- Recalculations will result in a farenheit and celcius column
Step 4: Plot the data using only farenheit¶
- Plot the farenheit data for Ramona, CA from 1998 to 2025
- We will not use celcius for this as US audiences understand farenheit
- When we plot we will not have python plot every single date as the graph will be illegible
- Instead we will use the average year using the mean() function
Step 5: Save the plot¶
- Save the Ramona CA average yearly temperature data and export as html
WARNING¶
- This code was run locally. If reproducing this project, follow the directions below for downloading the required NOAA/NCEI data for this portion of the project.
- Pay attention to your directories as well. Either delete the directories in code cell 2 or replace them with your own directory format.
# Step 0: libraries
# Libraries for working with NCEI data
from pathlib import Path
import pandas as pd
# Libraries for plotting and saving plot
import hvplot.pandas
import holoviews as hv
# Making trend (slope) lines
import numpy as np
from sklearn.metrics import r2_score
What the code in 'Step 0.1: Project Paths' does¶
In this step, we define reusable directory paths for the project. Setting these once at the top of the notebook helps ensure that data, figures, and exported files are saved in consistent locations across multiple notebooks. In future notebooks, you will see similar code, where other useful directories are made, for example figures.
# Step 0.1
# Project paths
PROJECT_ROOT = Path("..").resolve()
DATA_DIR = PROJECT_ROOT / "data"
TEMP_DIR = DATA_DIR / "temperature"
Selecting your own data from NOAA/NCEI¶
In this notebook I will be looking at temperature data for Ramona, CA. You can use any station data you would like for your own project. I have downloaded this data directly from NOAA. Follow the steps below for using your own data.
- Go to the NOAA National Centers fo Environmental Information, and from the 'Home' tab navigate to 'Climate Data Online'
- From here you will want to use the search tool which is available here: https://www.ncei.noaa.gov/cdo-web/search
- Under 'Select weather observation type/dataset' you will want to select 'daily summaries'
- For the 'date range' put in the month-day-year end and start dates you are interested in.
- For 'search for' set this to Stations
- Lastly enter a search term. If you already know your station id add it here, otherwise you can type in a location name and once you search it will show you all of the availabel stations tagged to that area.
- Regardless of which station you end up add to your cart, you want to ensure that you download your file as a CSV, and that the download includes the 'air temperature' option which will give you all the data you need for this project.
Ramona, CA station information¶
This station (GHCND:USW00053120) provides the most complete and continuous temperature record for the Ramona area, making it suitable for trend analysis from 1998–2025.
In this notebook I am using the following station:
Stations
GHCND:USW00053120
Begin Date 1998-04-16 00:00
End Date 2025-08-26 23:59
Data Types
TAVG TMAX TMIN
Units
Standard
Custom Flag(s)
Station Name
Step 1: Import data into python¶
- index_col='DATE' – this sets the DATE column as the index. Needed for subsetting and resampling later on
- parse_dates=True – this lets python know that you are working with time-series data, and values in the indexed column are date time objects
- na_values=['NaN'] – this lets python know how to handle missing values
# Step 1: Import data into python
# Using your downloaded data we will pull and read the CSV
## NOTE ##
# It is good practice to rename your data files into something descriptive and easy to identify
# For example, I have renamed the Ramona, CA station data as 'ncei-climate-ramona.csv'
# Instead of typing this out we will create a var called csv_path
# Specify path
csv_path = TEMP_DIR / "ncei-climate-ramona.csv"
ramona_climate_df = pd.read_csv(
csv_path,
index_col="DATE",
parse_dates=True,
na_values=["NaN"]
)
ramona_climate_df.head()
| STATION | NAME | TAVG | TMAX | TMIN | |
|---|---|---|---|---|---|
| DATE | |||||
| 1998-04-16 | USW00053120 | RAMONA AIRPORT, CA US | 48.0 | 61.0 | 34.0 |
| 1998-04-17 | USW00053120 | RAMONA AIRPORT, CA US | 53.0 | 69.0 | 36.0 |
| 1998-04-18 | USW00053120 | RAMONA AIRPORT, CA US | 55.0 | 72.0 | 37.0 |
| 1998-04-19 | USW00053120 | RAMONA AIRPORT, CA US | 59.0 | 77.0 | 41.0 |
| 1998-04-20 | USW00053120 | RAMONA AIRPORT, CA US | 63.0 | 82.0 | 43.0 |
Step 2: clean the dataframe¶
- We will get rid of the unnecessary columns in this csv
- You can use double brackets ([[ and ]]) to select only the columns that you want from the dataframe
- When specifying a column name use single ' ' quotes not " " full quotes
# Step 2: Clean the dataframe
ramona_climate_max_min_df = ramona_climate_df[['TMAX', 'TMIN']]
# We are using TMAX and TMIN because we later want to plot the average yearly max-min temperature.
# This will help clarify if Ramona, CA is not only getting warmer, but how much hotter these temps are year to year.
# The same logic applies to how much that min temp is rising over time.
# We are generally more interested in the effects of these extremes when it comes to ecosystem responses.
# I also know the climate of Ramona, CA well. We generally have more extreme temperature swings in the winter and sping.
# In the summer we stay consistently hot and do not really cool off that much in the evenings.
# As a result, the TAVG will actually mask a lot of the seasonal swings that are important to fire season in Ramona, CA
# Especially during the months we see the most fire activity, which is October
# Call the new variable
ramona_climate_max_min_df
| TMAX | TMIN | |
|---|---|---|
| DATE | ||
| 1998-04-16 | 61.0 | 34.0 |
| 1998-04-17 | 69.0 | 36.0 |
| 1998-04-18 | 72.0 | 37.0 |
| 1998-04-19 | 77.0 | 41.0 |
| 1998-04-20 | 82.0 | 43.0 |
| ... | ... | ... |
| 2025-08-22 | 100.0 | 62.0 |
| 2025-08-23 | 103.0 | 61.0 |
| 2025-08-24 | 95.0 | 61.0 |
| 2025-08-25 | 95.0 | 63.0 |
| 2025-08-26 | 93.0 | 58.0 |
9947 rows × 2 columns
Step 3: convert temperature units¶
- If you remember from earlier, the units for the NCEI data was 'standard.' Check out the documentation for GCHNd data. I downloaded data with “standard” units; find out what that means for temperature.
- See the GCHNd documentation here: https://www.ncei.noaa.gov/data/global-historical-climatology-network-daily/doc/GHCND_documentation.pdf
- For the TOBS column this is reported in farencheit but we will also want celsius
- We will use some basic math to accomplish this in python
# Step 3: Convert temperature units
# Label the TMAX column as the correct temperature (temp_f)
climate_temp = ramona_climate_max_min_df.rename(columns={
"TMAX": "max_temp_f",
"TMIN": "min_temp_f"
})
# We will then convert temp_f to temp_c (celcius) using a basic equation
climate_temp['max_temp_c'] = ((climate_temp['max_temp_f']-32)*5/9)
climate_temp['min_temp_c'] = ((climate_temp['min_temp_f']-32)*5/9)
# Call the new var to see the date, temp_f and temp_c columns
climate_temp.head()
| max_temp_f | min_temp_f | max_temp_c | min_temp_c | |
|---|---|---|---|---|
| DATE | ||||
| 1998-04-16 | 61.0 | 34.0 | 16.111111 | 1.111111 |
| 1998-04-17 | 69.0 | 36.0 | 20.555556 | 2.222222 |
| 1998-04-18 | 72.0 | 37.0 | 22.222222 | 2.777778 |
| 1998-04-19 | 77.0 | 41.0 | 25.000000 | 5.000000 |
| 1998-04-20 | 82.0 | 43.0 | 27.777778 | 6.111111 |
Step 4: plot the data using only farenheit¶
- We do not want to plot out every single day of data for Ramona, CA from 1988-2025, so we will resample these dates by creating a new variable that reflects the annual average temperature for each year in the NCEI data.
- So for this data, we will resample to the start of the year or YS.
- To get the average, we will then tell python to use the mean() function.
Why resample to annual averages?¶
Daily data can be noisy and harder to interpret. To analyze long-term climate trends, we resample the dataset to yearly averages. This step allows us to detect underlying warming or cooling patterns over multiple decades.
# Step 4: Plot the data using only farenheit
# Make a new variable using climate_temp to have a new var ann_ramona_climate_df that calculates the average annual value of the temp
#'YS' is the start of the calendar year
# mean() will calculate the average temperature of both the temp_f and temp-c columns for var climate_temp
ann_ramona_climate_df = (
climate_temp
.resample('YS')
.mean()
)
# Here rename the index column from DATE to year so that the displayed table is easier to interpret
ann_ramona_climate_df.index = ann_ramona_climate_df.index.year
ann_ramona_climate_df.index.name = "year"
# Call the new var
ann_ramona_climate_df
# Note that we now have years listed with the max and min temps now
# Since we are using mean here we lose the high peaks Ramona, CA gets in the summers (usually over 100+)
| max_temp_f | min_temp_f | max_temp_c | min_temp_c | |
|---|---|---|---|---|
| year | ||||
| 1998 | 74.799127 | 45.506550 | 23.777293 | 7.503639 |
| 1999 | 75.240331 | 42.930939 | 24.022406 | 6.072744 |
| 2000 | 75.763085 | 45.421488 | 24.312825 | 7.456382 |
| 2001 | 73.967033 | 45.068681 | 23.315018 | 7.260379 |
| 2002 | 74.620879 | 44.258242 | 23.678266 | 6.810134 |
| 2003 | 75.513889 | 45.663889 | 24.174383 | 7.591049 |
| 2004 | 75.456284 | 45.084699 | 24.142380 | 7.269277 |
| 2005 | 75.476712 | 45.643836 | 24.153729 | 7.579909 |
| 2006 | 76.753425 | 44.879452 | 24.863014 | 7.155251 |
| 2007 | 76.723757 | 44.475138 | 24.846532 | 6.930632 |
| 2008 | 77.543175 | 46.175487 | 25.301764 | 7.875271 |
| 2009 | 76.894150 | 45.194986 | 24.941195 | 7.330548 |
| 2010 | 74.341737 | 44.593838 | 23.523187 | 6.996576 |
| 2011 | 75.332418 | 43.016529 | 24.073565 | 6.120294 |
| 2012 | 77.463014 | 44.630137 | 25.257230 | 7.016743 |
| 2013 | 76.805479 | 44.586301 | 24.891933 | 6.992390 |
| 2014 | 79.200000 | 47.158904 | 26.222222 | 8.421613 |
| 2015 | 78.076712 | 47.284932 | 25.598174 | 8.491629 |
| 2016 | 77.969863 | 46.405479 | 25.538813 | 8.003044 |
| 2017 | 78.952381 | 47.678771 | 26.084656 | 8.710428 |
| 2018 | 78.589041 | 47.378082 | 25.882801 | 8.543379 |
| 2019 | 75.945205 | 46.126027 | 24.414003 | 7.847793 |
| 2020 | 78.409836 | 44.877049 | 25.783242 | 7.153916 |
| 2021 | 77.175824 | 44.690411 | 25.097680 | 7.050228 |
| 2022 | 78.755495 | 46.453297 | 25.975275 | 8.029609 |
| 2023 | 75.458791 | 45.570248 | 24.143773 | 7.539027 |
| 2024 | 78.388430 | 45.484848 | 25.771350 | 7.491582 |
| 2025 | 77.710084 | 45.084034 | 25.394491 | 7.268908 |
Understanding the trend calculations below¶
To quantify long-term temperature change, we calculate:
- Slope: the average change in °F per year
- Intercept: the model-predicted value at year 0 (which is treated as literally 0AD so don't worry about the intercept values)
- R²: how well the trend line explains the variation
These values help assess how rapidly Ramona is warming.
#### CONT ####
# Step 4: Plot the data using only farenheit
# Use new var ann_ramona_climate_df to plot the annual data
# Give this plot a descriptive title and label the x and y axis
# By setting y to max_temp_f it will display only the max farenheit data
# We will do this again to make a separate figure for min_temp_f
### Why create individual figures? ###
# Mainly because the range of the graph (y) with both plotted will make it harder to see the magnitude of peaks and lows
# Creating the slope for the ramona_climate_max_f figure
# Extract values
x = ann_ramona_climate_df.index.values.astype(float)
y_max = ann_ramona_climate_df["max_temp_f"].values
# Compute linear regression (slope & intercept)
slope_max, intercept_max = np.polyfit(x, y_max, 1)
y_max_trend = slope_max * x + intercept_max
# Use hvplot not plot so that the resulting figure is interactive
ramona_climate_max_f = ann_ramona_climate_df.hvplot(
y=["max_temp_f"],
title= 'Ramona, CA Average Max Yearly Temperature',
xlabel='Year',
ylabel='Temperature (°F)',
line_width=2,
)
# Trend line as separate HoloViews Curve
trend_line_max = hv.Curve(
(x, y_max_trend),
kdims="Year", vdims="Temperature (°F)"
).opts(color="red", line_width=2, line_dash="dashed", alpha=0.7)
# Overlay the trend on only this figure
ramona_climate_max_f_with_trend = ramona_climate_max_f * trend_line_max
# Display it
ramona_climate_max_f_with_trend
# We can also call the slope, intercept and R2 per year and per decade
# In the cell above we already calculated the slope_max and intercept_max so we reuse them here
# Compute predicted values for R²
y_max_pred = slope_max * x + intercept_max
# R-squared
r2_max = r2_score(y_max, y_max_pred)
# Decadal slope
slope_max_decade = slope_max * 10
# Print summary
# What this print code does it automatically label the values into something legible for us to use
# This will display everything in appropriate units
# The intercept isn't helpful here because its predicting the temperature at year=0 (which is 0 AD) and entirely meaningless for this
print("=== Ramona, CA Annual Max Temperature Trend ===")
print(f"Slope (per year): {slope_max:.4f} °F/year")
print(f"Slope (per decade): {slope_max_decade:.4f} °F/decade")
print(f"Intercept: {intercept_max:.2f}")
print(f"R²: {r2_max:.4f}")
=== Ramona, CA Annual Max Temperature Trend === Slope (per year): 0.1250 °F/year Slope (per decade): 1.2496 °F/decade Intercept: -174.67 R²: 0.4500
# We can also see it without the trend line
# Call the plot variable with no trend
ramona_climate_max_f
#### CONT ####
# Step 4: Plot the data using only farenheit
# Now we will do the same code as above but just using temp_min_f instead
# Use new var ann_ramona_climate_df to plot the annual data
# Give this plot a descriptive title and label the x and y axis
# By setting y to min_temp_f it will display only the min farenheit data
# Extract values for regression
x = ann_ramona_climate_df.index.values.astype(float)
y_min = ann_ramona_climate_df["min_temp_f"].values
# Compute linear regression (slope & intercept)
slope_min, intercept_min = np.polyfit(x, y_min, 1)
y_min_trend = slope_min * x + intercept_min
# Plot actual values (min temperatures)
ramona_climate_min_f = ann_ramona_climate_df.hvplot(
y="min_temp_f",
title="Ramona, CA Average Min Yearly Temperature",
xlabel="Year",
ylabel="Temperature (°F)",
line_width=2,
)
# Trend line as separate HoloViews Curve
trend_line_min = hv.Curve(
(x, y_min_trend),
kdims="Year",
vdims="Temperature (°F)"
).opts(
color="red",
line_width=2,
line_dash="dashed",
alpha=0.7
)
# Overlay the trend on only this figure
ramona_climate_min_f_with_trend = ramona_climate_min_f * trend_line_min
# Display it
ramona_climate_min_f_with_trend
#### CONT ####
# Step 4: Plot the data using only farenheit
# We can also call the slope, intercept and R2 per year and per decade
# In the cell above we already calculated the slope_min and intercept_min so we reuse them here
# Predicted values for R²
y_min_pred = slope_min * x + intercept_min
# R-squared
r2_min = r2_score(y_min, y_min_pred)
# Decadal slope
slope_min_decade = slope_min * 10
# Print summary
print("=== Ramona, CA Annual Min Temperature Trend ===")
print(f"Slope (per year): {slope_min:.4f} °F/year")
print(f"Slope (per decade): {slope_min_decade:.4f} °F/decade")
print(f"Intercept: {intercept_min:.2f}")
print(f"R²: {r2_min:.4f}")
=== Ramona, CA Annual Min Temperature Trend === Slope (per year): 0.0507 °F/year Slope (per decade): 0.5071 °F/decade Intercept: -56.59 R²: 0.1290
# We can also see it without the trend line
# Call the plot variable with no trend
ramona_climate_min_f
#### CONT ####
# Step 4: Plot the data using only farenheit
# To make it easier to view these figures we can stack them
# When we save these as html and static images we can save them both individually and joined
# You will notice that I have labeled these figures differently. That is because I don't want the trend line showing here
# Like we have in the independent figures above.
# Specifying .cols() makes python stack these top and bottom
ramona_max_temp = ann_ramona_climate_df.hvplot(
y="max_temp_f",
title="Annual Average Maximum Temperature",
xlabel="Year", ylabel="Temperature (°F)", line_width=2,
)
ramona_min_temp = ann_ramona_climate_df.hvplot(
y="min_temp_f",
title="Annual Average Minimum Temperature",
xlabel="Year", ylabel="Temperature (°F)", line_width=2,
)
(ramona_max_temp + ramona_min_temp).cols(1)
Step: 5 Save the Plot¶
To make results reusable in later notebooks and for the final report, we save the annual trend figures as .html files. These can be opened outside Jupyter and retain their interactivity.
# Step 5: Save the plots
FIG_DIR = PROJECT_ROOT / "figures"
FIG_DIR.mkdir(exist_ok=True)
# Interacitve figures with NO trend lines
hv.save(ramona_climate_min_f, FIG_DIR / "Ramona_avg_min_yr_temp_f.html")
hv.save(ramona_climate_max_f, FIG_DIR / "Ramona_avg_max_yr_temp_f.html")
combined = (ramona_max_temp + ramona_min_temp).cols(1)
hv.save(combined, FIG_DIR / "ramona_max_min_temps_vertical.html")
# Interactive figures WITH trend lines
hv.save(ramona_climate_max_f_with_trend, FIG_DIR / "ramona_max_temp_trend.html")
hv.save(ramona_climate_min_f_with_trend, FIG_DIR / "ramona_min_temp_trend.html")
# Static figures with NO trend lines
hv.save(ramona_climate_max_f, FIG_DIR / "ramona_avg_max_temp_f_year.png", fmt="png")
hv.save(ramona_climate_min_f, FIG_DIR / "ramona_avg_min_temp_f_year.png", fmt="png")
hv.save(combined, FIG_DIR / "ramona_max_min_stacked.png", fmt="png")
# Static figures WITH trend lines
hv.save(ramona_climate_max_f_with_trend, FIG_DIR / "ramona_max_temp_f_trend.png", fmt="png")
hv.save(ramona_climate_min_f_with_trend, FIG_DIR / "ramona_min_temp_f_trend.png", fmt="png")
Storing variables for later notebooks¶
The %store command makes the cleaned annual temperature dataframe available to other notebooks in this project without needing to reload or reprocess the original data.
You should not store plots.
# Store any variables for future notebooks
%store ann_ramona_climate_df
Stored 'ann_ramona_climate_df' (DataFrame)
Ramona, CA: The hot is getting hotter and the cold is getting warmer¶
Temperature Trends in Ramona, CA (1998–2025)¶
Analysis of annual maximum and minimum temperatures from the NOAA NCEI Ramona Airport station shows clear evidence of warming over the past 25 years. Both daytime highs (TMAX) and nighttime lows (TMIN) have increased, although at different rates.
Minimum Temperature Trend (Nighttime Lows)¶
Nighttime temperatures in Ramona exhibit a modest but consistent upward trend, which means our cool nights are getting warmer.
- Warming rate: +0.0507 °F per year
- Equivalent to: +0.507 °F per decade
- R²: 0.129
The relatively low R² indicates greater year-to-year variability, but the long-term trend still points toward steady warming. Increasing minimum temperatures reflect reduced nighttime cooling, and perhaps broader issues of climate change. If we take our yearly warming rate (0.0507) and multiply it by the number of years (27), the (modeled) minimum temperature increased by 1.37 °F.
Maximum Temperature Trend (Daytime Highs)¶
Daytime temperatures show a stronger and more statistically robust increase, which means our hot days are getting hotter
- Warming rate: +0.1250 °F per year
- Equivalent to: +1.2496 °F per decade
- R²: 0.450
The higher R² demonstrates a clearer linear warming pattern in daytime highs, consistent with observed regional heat intensification in inland Southern California. The other thing of note, is that Ramona is warming RAPIDLY. If we take our yearly warming rate (0.1250) and multiply it by the number of years (27), the (modeled) maximum temperature increased by 3.38 °F. That is very fast for a 27 year period.
Overall Implications¶
Taken together, the results show that Ramona is warming, with:
- Faster increases in daytime maximum temperatures
- Steady but slower increases in nighttime minimum temperatures
- From 1998 to 2025, Ramona’s daytime high temperatures increased by about 3.4 °F, while nighttime minimum temperatures increased by about 1.4 °F.
These patterns have important implications for:
- Fire risk: hotter, drier afternoons increase ignition and spread potential (especially with Southern California's Santa Ana winds)
- Ecosystem stress: vegetation faces higher evaporative demand (meaning when we do our NDVI notebooks we should see lower NDVI over time)
- Human exposure: increases in heat-related illnesses (i.e., heat stoke)
So why have we spent our first notebook looking at climate for Ramona, CA if our focus in on fire?¶
The stronger rise in maximum temperatures aligns with broader climate trends across Southern California and may compound post-fire recovery challenges in areas affected by the 2003 Cedar Fire and 2007 Witch Fire. Knowing this climate context is important for helping us interpret the NDVI boundary data we analyze, and the changes in landcover (i.e., less drought tolerant species may disappear from the region over time).