Cross-Correlation#
Analyze the time-lagged relationship between two signals using cross-correlation.
Goal
Analyze the time-lagged correlation between outdoor temperature and indoor temperature (Flat A). Cross-correlation reveals how strongly and with what delay the outdoor climate influences the indoor environment.
Data Basis
Datasets:
flatTempHum.csv— Hourly indoor temperature (FlatA_Temp)centralOutsideTemp.csv— Hourly outdoor temperature (centralOutsideTemp)
Time range: Overlapping period of both datasets
Lag range: -48 to +48 hours
Data Preparation#
import pandas as pd
base_url = "https://raw.githubusercontent.com/retomarek/edap/main/edap/sampleData/"
# Load indoor temperature
df_indoor = pd.read_csv(base_url + "flatTempHum.csv", sep=";", usecols=["time", "FlatA_Temp"])
df_indoor["time"] = pd.to_datetime(df_indoor["time"])
df_indoor = df_indoor.set_index("time", drop=True)
# Load outdoor temperature
df_outdoor = pd.read_csv(base_url + "centralOutsideTemp.csv", sep=";")
df_outdoor["time"] = pd.to_datetime(df_outdoor["time"])
df_outdoor = df_outdoor.set_index("time", drop=True)
df_indoor.head()
| FlatA_Temp | |
|---|---|
| time | |
| 2018-10-03 00:00:00 | 24.43 |
| 2018-10-03 01:00:00 | 24.40 |
| 2018-10-03 02:00:00 | 24.40 |
| 2018-10-03 03:00:00 | 24.40 |
| 2018-10-03 04:00:00 | 24.40 |
# Join on the time index and keep a regular hourly grid: missing hours stay NaN,
# so that .shift(1) below really moves the data by one hour
df = df_indoor.join(df_outdoor, how="inner").asfreq("h")
df = df.rename(columns={"centralOutsideTemp": "outdoor", "FlatA_Temp": "indoor"})
print(f"Combined dataset: {len(df)} hours, {df['indoor'].isna().sum()} without indoor value")
df.head()
Combined dataset: 17172 hours, 688 without indoor value
| indoor | outdoor | |
|---|---|---|
| time | ||
| 2018-10-03 00:00:00 | 24.43 | 11.80 |
| 2018-10-03 01:00:00 | 24.40 | 11.25 |
| 2018-10-03 02:00:00 | 24.40 | 11.45 |
| 2018-10-03 03:00:00 | 24.40 | 11.40 |
| 2018-10-03 04:00:00 | 24.40 | 11.10 |
Visualization#
Both temperatures follow the annual cycle (warm in summer, cold in winter), so their raw
values correlate strongly at every lag. To see the delay of the building, we correlate the
hour-to-hour changes (.diff()) instead. indoor.corr(outdoor.shift(lag)) compares the
indoor change with the outdoor change lag hours earlier: a peak at a positive lag means
the indoor temperature follows the outdoor temperature with that delay.
import plotly.express as px
changes = df.diff() # hour-to-hour changes, NaN next to missing hours
max_lag = 48
lags = range(-max_lag, max_lag + 1)
correlations = [changes["indoor"].corr(changes["outdoor"].shift(lag)) for lag in lags]
df_corr = pd.DataFrame({"lag_hours": list(lags), "correlation": correlations})
best = df_corr.loc[df_corr["correlation"].idxmax()]
print(f"Maximum correlation: {best['correlation']:.2f} at lag = {int(best['lag_hours'])} hours")
Maximum correlation: 0.22 at lag = 4 hours
fig = px.bar(
df_corr,
x="lag_hours",
y="correlation",
title="Hourly Changes: Outdoor vs. Indoor Temperature (Flat A)",
labels={"lag_hours": "Lag [hours]", "correlation": "Pearson Correlation"},
)
fig.update_layout(bargap=0)
fig.show()
The indoor temperature follows the outdoor temperature with a delay of a few hours (thermal inertia of the building). The correlation is weak because heating and occupants change the indoor temperature much more than the weather does from one hour to the next.
Customization
Adjust
max_lagto explore longer or shorter delay windows.Correlate the raw values (
dfinstead ofchanges) to see how the shared annual cycle hides the delay: the correlation is high at every lag.Replace
px.barwithpx.linefor a smoother correlation curve.