Historical high water levels of the river Ahr

Topic: Why was the "100-year" flood event of the river Ahr in Germany on 14-15th July 2021 so catastrophic?
Author: Van Oproy Kurt

Preliminary notes

  • The discharge values are the calculated daily or hourly averages, based on the water level measurement.
    • The datasets with hourly averages contain only data of the last 31 days.
  • The data and descriptions in historical records of similar floods had not been included in the top 10 high water dataset for making predictions.
    • The data which the RLP administration for environment used, had only a timespan going back 75 years. Or much less.
  • The rainfall amounts of 150-180 mm per day, which in some places occured in a few hours time, were predicted in time.
  • Some of the hydrometers broke down before the peak flow. One measurestation was completely washed away.
    • Some stations had direct communication links, but they broke down with the collapse of the telecom and electricity infrastructure.
    • So, the early warning system was partly cut down before the peak flow.
    • Nonetheless, I found 1 station that had all the values of the peak wave, so I'll try to construct a discharge curve of the composite water mass.

References

  • Reconstructing peak discharges of historic floods of the river Ahr, Germany, 2014-01-05, Thomas Roggenkamp and Jürgen Herget
  • Ahr-Hochwasser am 12. 13. Juni 1910 forderte 52 Menschenleben, Leonhard Janta, Helmut Poppelreuter, Heimatjahrbuch Kreis Ahrweiler 2010
  • Ahrserie_Katastrophen_28_Juni_2014.pdf, NR. 139. MITTWOCH 18 JUNI 2014, Rhein-Zeitung Epaper - Mittelrhein-Verlag, Koblenz,Germany, Petra Ochs
  • Die Ahr und ihre Hochwässer in alten Quellen. Karl August Seel, Heimatjahrbuch des Kreises Ahrweiler 40, 1983
  • Das Hochwasser von 1804 im Kreise Ahrweiler, Dr. HANS FRICK, hjb1955
    • This was an update of his original publication from 1929.
  • Das Naturschutzgebiet Ahrschleife bei Altenahr, Teil 2, Büchs, W.
  • Floods in Germany - Analyses of Trends, T. Petrow: Dr. diss.
  • Characteristics and process controls of statistical flood moments in Europe – a data based analysis, 2020, David Lun, Alberto Viglione, Miriam Bertola, Jürgen Komma, Juraj Parajka1, Peter Valent, Günter Blöschl, https://doi.org/10.5194/hess-2020-600
  • RLP administration for environment

Post-flood event references

  • Hochwasser Mitteleuropa, Juli 2021 (Deutschland), CEDIM Forensic Disaster Analysis (FDA) Group, 21. Juli 2021 – Bericht Nr. 1 "Nordrhein-Westfalen & Rheinland-Pfalz"
  • Déjà-vu der Katastrophe, OLIVER SCHLÖMER, JENS GIESEL und MANFRED LINDINGER · 5. August 2021
  • Katastrophale Hochwasser im Ahrtal 2021, 1910, 1804, 1719 und 1601, reitschuster.de
Slope of the steep hill at Dernau
In [28]:
(300-160)/518
Out[28]:
0.2702702702702703
import this
In [2]:
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
sns.set()
sns.set_context('notebook')
np.set_printoptions(threshold=20, precision=2, suppress=True)
pd.options.display.max_rows = 57
pd.options.display.max_columns = 38
pd.set_option('precision', 2)

from IPython.display import Image, display
%config InlineBackend.figure_format = 'retina'
from IPython.core.display import display, HTML
display(HTML("<style>.container {width:72% !important;}</style>"))
In [3]:
%autosave 400
In [3]:
Ahr =pd.read_csv(r"Ahr Aktuelle Wasserstände.csv",sep="\t", skiprows=2,usecols=[1,2],index_col=0,na_values= "-"); # header=0,sep=\";\",\n",
Ahr.index = pd.to_datetime(Ahr.index, format="%d.%m.%Y %H:%M")   # parse_dates=["Datum"]

Abfluss =pd.read_csv(r"Ahr Abfluss.csv",sep="\t", index_col=1,na_values= "-",decimal=","); #parse_dates=["Datum"],
Abfluss.index = pd.to_datetime(Abfluss.index, format="%d.%m.%Y %H:%M")
Ahr.head(10)
Out[3]:
Wasserstand in cm
Datum
2021-06-24 17:15:00 59.0
2021-06-24 17:30:00 60.0
2021-06-24 17:45:00 60.0
2021-06-24 18:00:00 60.0
2021-06-24 18:15:00 60.0
2021-06-24 18:30:00 60.0
2021-06-24 18:45:00 61.0
2021-06-24 19:00:00 62.0
2021-06-24 19:15:00 62.0
2021-06-24 19:30:00 62.0

Pegel Altenahr

In [42]:
Ahr.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 2204 entries, 2021-06-24 17:15:00 to 2021-07-25 16:00:00
Data columns (total 1 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   Wasserstand in cm  2059 non-null   float64
dtypes: float64(1)
memory usage: 34.4 KB
In [42]:
Ahrstats =pd.read_csv(r"Ahr.csv", skiprows=2,sep="\t",index_col=0,decimal="," ,); 
Hochwasser =pd.read_csv(r"AhrHochwasserereignisse19462019.csv",parse_dates=["Datum"],skiprows=2,sep="\t",index_col=1,decimal=",",
                        engine='python'); #skipfooter=1
#floods[\"Date\"] = pd.to_datetime( River_Arno.Date, format='%d/%m/%Y' ) # euro dates\n",
#floods.set_index(\"Date\", inplace=True)\n",
Ahrstats.head(10)
Out[42]:
Abfluss [m³/s] Abflussspende [l/s km²]
Jährlichkeiten
2 93.5 125
5 125.0 168
10 149.0 200
20 176.0 236
25 185.0 248
50 212.0 284
100 241.0 323

Since the calibration curve for the high water levels to estimate the interval of a "100 year flood" is actually based on a shorter period, in fact 1946-2019, we have to correct the timespan accordingly...

In [78]:
Ahrstats["days"]=Ahrstats.index*365
#Ahrstats["year"]= Ahrstats.Jährlichkeiten 
Ahrstats["years"]= Ahrstats.index*((2021.5 -1946)/100)
In [79]:
Ahrstats.head(11)
Out[79]:
Abfluss [m³/s] Abflussspende [l/s km²] days years
Jährlichkeiten
2 93.5 125 730 1.51
5 125.0 168 1825 3.77
10 149.0 200 3650 7.55
20 176.0 236 7300 15.10
25 185.0 248 9125 18.88
50 212.0 284 18250 37.75
100 241.0 323 36500 75.50

Precipitation data

Daily Barweiler

In [8]:
Barweiler =pd.read_csv(r"stundenwerte_RR_03660_akt\produkt_rr_stunde_20200130_20210801_03660.txt",sep=";",index_col=1,
                       na_values= "-999",); # header=0,sep=\";\",\n", ,usecols=[2,3], parse_dates=["MESS_DATUM"]
Barweiler.index = pd.to_datetime(Barweiler.index, format="%Y%m%d%H") # -%M-
Barweiler.head(10)
Out[8]:
STATIONS_ID QN_8 R1 RS_IND WRTR eor
MESS_DATUM
2020-01-30 00:00:00 3660 3 0.0 0.0 NaN eor
2020-01-30 01:00:00 3660 3 0.0 0.0 0.0 eor
2020-01-30 02:00:00 3660 3 0.0 1.0 6.0 eor
2020-01-30 03:00:00 3660 3 0.0 0.0 NaN eor
2020-01-30 04:00:00 3660 3 0.0 0.0 0.0 eor
2020-01-30 05:00:00 3660 3 0.0 0.0 0.0 eor
2020-01-30 06:00:00 3660 3 0.0 0.0 NaN eor
2020-01-30 07:00:00 3660 3 0.0 0.0 0.0 eor
2020-01-30 08:00:00 3660 3 0.0 0.0 0.0 eor
2020-01-30 09:00:00 3660 3 0.0 0.0 NaN eor
In [8]:
Barweiler.tail(3)
Out[8]:
STATIONS_ID QN_8 R1 RS_IND WRTR eor
MESS_DATUM
2021-07-31 18:00:00 3660 1 0.0 1.0 NaN eor
2021-07-31 19:00:00 3660 1 0.0 1.0 6.0 eor
2021-07-31 20:00:00 3660 1 0.0 1.0 6.0 eor
2021-07-31 21:00:00 3660 1 0.0 0.0 NaN eor
2021-07-31 22:00:00 3660 1 0.0 0.0 0.0 eor
2021-07-31 23:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 00:00:00 3660 1 0.0 0.0 NaN eor
2021-08-01 01:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 02:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 03:00:00 3660 1 0.0 0.0 NaN eor
2021-08-01 04:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 05:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 06:00:00 3660 1 0.0 0.0 NaN eor
2021-08-01 07:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 08:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 09:00:00 3660 1 0.0 0.0 NaN eor
2021-08-01 10:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 11:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 12:00:00 3660 1 0.0 0.0 NaN eor
2021-08-01 13:00:00 3660 1 0.0 1.0 6.0 eor
2021-08-01 14:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 15:00:00 3660 1 0.0 1.0 NaN eor
2021-08-01 16:00:00 3660 1 1.4 1.0 6.0 eor
2021-08-01 17:00:00 3660 1 0.0 1.0 6.0 eor
2021-08-01 18:00:00 3660 1 0.0 0.0 NaN eor
2021-08-01 19:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 20:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 21:00:00 3660 1 0.0 0.0 NaN eor
2021-08-01 22:00:00 3660 1 0.0 0.0 0.0 eor
2021-08-01 23:00:00 3660 1 0.0 0.0 0.0 eor
In [30]:
Barweiler["  R1"].plot(); 
In [42]:
Barweiler["2021-07-07":"2021-07-16"]["  R1"].plot(); 
In [31]:
Barweiler.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 13055 entries, 2020-01-30 00:00:00 to 2021-08-01 23:00:00
Data columns (total 6 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   STATIONS_ID  13055 non-null  int64  
 1   QN_8         13055 non-null  int64  
 2     R1         13039 non-null  float64
 3   RS_IND       13039 non-null  float64
 4   WRTR         8709 non-null   float64
 5   eor          13055 non-null  object 
dtypes: float64(3), int64(2), object(1)
memory usage: 713.9+ KB
In [34]:
Barweiler["  R1"].resample("D").sum().plot(); 
In [38]:
BarweilerD= Barweiler["  R1"].resample("D").sum()
BarweilerD.rolling(7).sum().plot(); 
In [39]:
BarweilerD.rolling(14).sum().plot(); 

Daily Adenauer

In [46]:
Adenauer =pd.read_csv(r"stundenwerte_RR_03490_akt\produkt_rr_stunde_20200130_20210715_03490.txt",sep=";",index_col=1,
                       na_values= "-999",); # header=0,sep=\";\",\n", ,usecols=[2,3], parse_dates=["MESS_DATUM"]
Adenauer.index = pd.to_datetime(Adenauer.index, format="%Y%m%d%H") # -%M-
Adenauer.head(10)
Out[46]:
STATIONS_ID QN_8 R1 RS_IND WRTR eor
MESS_DATUM
2020-01-30 00:00:00 3490 3 0.0 0 NaN eor
2020-01-30 01:00:00 3490 3 0.0 0 NaN eor
2020-01-30 02:00:00 3490 3 0.0 0 NaN eor
2020-01-30 03:00:00 3490 3 0.0 0 NaN eor
2020-01-30 04:00:00 3490 3 0.0 0 NaN eor
2020-01-30 05:00:00 3490 3 0.0 0 NaN eor
2020-01-30 06:00:00 3490 3 0.0 0 NaN eor
2020-01-30 07:00:00 3490 3 0.0 0 NaN eor
2020-01-30 08:00:00 3490 3 0.0 0 NaN eor
2020-01-30 09:00:00 3490 3 0.0 0 NaN eor
In [47]:
Adenauer.tail(10)
Out[47]:
STATIONS_ID QN_8 R1 RS_IND WRTR eor
MESS_DATUM
2021-07-14 21:00:00 3490 1 0.0 1 NaN eor
2021-07-14 22:00:00 3490 1 0.0 0 NaN eor
2021-07-14 23:00:00 3490 1 0.0 0 NaN eor
2021-07-15 00:00:00 3490 1 0.0 0 NaN eor
2021-07-15 01:00:00 3490 1 0.0 0 NaN eor
2021-07-15 02:00:00 3490 1 0.0 0 NaN eor
2021-07-15 03:00:00 3490 1 0.0 0 NaN eor
2021-07-15 04:00:00 3490 1 0.0 0 NaN eor
2021-07-15 05:00:00 3490 1 0.0 0 NaN eor
2021-07-15 06:00:00 3490 1 0.0 0 NaN eor
In [43]:
Adenauer["2021-07-07":"2021-07-16"]["  R1"].plot(); 
In [48]:
Adenauer.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 11795 entries, 2020-01-30 00:00:00 to 2021-07-15 06:00:00
Data columns (total 6 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   STATIONS_ID  11795 non-null  int64  
 1   QN_8         11795 non-null  int64  
 2     R1         11795 non-null  float64
 3   RS_IND       11795 non-null  int64  
 4   WRTR         0 non-null      float64
 5   eor          11795 non-null  object 
dtypes: float64(2), int64(3), object(1)
memory usage: 645.0+ KB
In [49]:
AdenauerD= Adenauer.iloc[:,2].resample("D").sum()
BarweilerD= Barweiler.iloc[:,2].resample("D").sum()
BarweilerD["2021-07-01":"2021-07-16"].plot(); 
In [50]:
AdenauerD["2021-07-01":"2021-07-16"].plot(); 
In [52]:
AhrD= Ahr.iloc[:,0].resample("D").sum()/24
AhrD.loc["2021-07-01":"2021-07-16"].plot(); 
In [53]:
AhrD.loc["2021-07-01":"2021-07-16"]
Out[53]:
Datum
2021-07-01    285.12
2021-07-02    252.54
2021-07-03    231.12
2021-07-04    227.54
2021-07-05    245.21
2021-07-06    232.71
2021-07-07    228.25
2021-07-08    232.08
2021-07-09    320.54
2021-07-10    302.58
2021-07-11    295.33
2021-07-12    273.54
2021-07-13    299.62
2021-07-14    601.38
2021-07-15      0.00
2021-07-16      0.00
Freq: D, Name: Wasserstand in cm, dtype: float64
In [54]:
AhrH= Ahr.iloc[:,0]#.resample("H").mean()
AhrH.loc["2021-07-01":"2021-07-16"].plot(); 
In [41]:
Ahr.loc["2021-07-01":"2021-07-16"].plot(); 
Out[41]:
<AxesSubplot:xlabel='Datum'>

Müsch has 352 km² drainage area, and suppose 100 liter fell in 2 days time:

In [46]:
100*1000*1000/1000*352/(3600*24)
Out[46]:
407.4074074074074

3500 m³ on 2days time needs a discharge of 400 cum/sec.
Datum Abfluss in m3/s Abflussspende in L/(s*km2) Wasserstand in cm
02.06.2016 132 374 273

In [47]:
170*1000*1000/1000*352/(3600*24)
Out[47]:
692.5925925925926
In [ ]:
 

Soil moisture content

Water content in the upper soil layers 10 cm thick, up till 60 cm deep.

In [9]:
soil =pd.read_csv(r"derived_germany_soil_daily_recent_3660.txt\derived_germany__soil__daily__recent__3660.txt",sep=";",index_col=1,
                       na_values= "-999",); # header=0,sep=\";\",\n", ,usecols=[2,3], parse_dates=["MESS_DATUM"]
soil.index = pd.to_datetime(soil.index, format="%Y%m%d") # -%M-
soil.loc["July 2021"].head(16)
Out[9]:
Stationsindex VGSL VPGB VPGH TS05 TS10 TS20 TS50 TS100 ZFUMI BF10 BF20 BF30 BF40 BF50 BF60 BFGSL BFGLS TSLS05 TSSL05 ZTKMI ZTUMI VPGPM VPMB VPWB VPZB VGLS VWLS VWSL BFWLS BFWSL eor
Datum
2021-07-01 3660 1.0 1.0 1.0 14.6 15.3 16.7 18.0 16.2 0 94 83 73 77 86 92 84 86 14.1 14.5 0 0 1.4 0.9 0.9 1.1 1.0 0.9 0.9 69 77 eor ...
2021-07-02 3660 2.5 2.8 2.5 18.3 17.7 17.0 17.2 16.2 0 87 81 74 77 86 92 82 83 17.6 17.3 0 0 3.1 2.5 2.4 3.2 2.4 2.3 2.3 66 76 eor ...
2021-07-03 3660 3.0 3.6 3.1 22.0 21.0 19.3 17.2 16.0 0 78 77 74 77 85 91 80 80 21.3 21.4 0 0 3.7 3.4 3.3 4.0 2.9 3.0 3.1 63 74 eor ...
2021-07-04 3660 1.9 1.9 0.8 20.0 20.2 19.9 17.9 15.9 0 105 94 76 77 85 91 88 92 19.6 19.6 0 0 2.2 1.7 2.0 2.1 1.9 2.0 2.0 75 80 eor ...
2021-07-05 3660 2.1 2.1 0.6 18.2 18.3 18.4 17.8 16.0 0 102 91 78 77 85 91 87 91 17.8 18.1 0 0 2.1 1.8 2.1 2.3 2.1 2.1 2.1 74 80 eor ...
2021-07-06 3660 3.1 3.1 2.0 18.5 18.4 18.3 17.6 16.0 0 103 94 80 78 84 91 88 92 18.2 18.1 0 0 3.0 2.8 3.1 3.8 3.2 3.1 3.1 75 80 eor ...
2021-07-07 3660 3.3 3.4 2.6 20.2 19.6 18.7 17.5 16.0 0 93 90 81 78 84 91 86 88 19.5 19.8 0 0 3.6 3.2 2.6 3.9 3.2 2.6 2.6 72 79 eor ...
2021-07-08 3660 1.1 1.1 0.2 17.8 18.2 18.6 17.7 16.0 0 110 114 110 96 84 91 101 114 17.3 17.5 0 0 1.5 1.2 1.1 1.3 1.1 1.1 1.1 97 93 eor ...
2021-07-09 3660 1.4 1.4 1.5 17.5 17.6 17.7 17.5 16.0 0 105 108 108 102 89 90 101 113 17.1 17.4 0 0 1.7 1.4 1.4 1.6 1.4 1.4 1.4 96 93 eor ...
2021-07-10 3660 3.1 3.1 3.3 20.4 19.7 18.5 17.3 16.0 0 98 102 104 103 94 91 99 108 20.2 19.9 0 0 3.0 3.1 2.6 3.6 3.1 2.6 2.6 94 92 eor ...
2021-07-11 3660 1.8 1.9 2.0 19.7 19.6 19.2 17.5 16.0 0 94 100 104 102 95 92 98 104 19.2 19.2 0 0 2.3 1.9 1.6 2.2 1.8 1.6 1.6 92 91 eor ...
2021-07-12 3660 2.0 2.1 1.7 19.6 19.4 18.9 17.7 16.0 0 91 98 103 101 95 92 97 101 19.1 19.2 0 0 2.3 2.0 1.6 2.5 2.0 1.6 1.6 90 90 eor ...
2021-07-13 3660 0.9 0.9 0.4 18.0 18.2 18.5 17.7 16.0 0 110 116 116 112 104 97 109 121 17.7 17.8 0 0 1.0 1.0 1.1 1.1 0.9 1.1 1.1 111 102 eor ...
2021-07-14 3660 0.5 0.5 0.1 15.9 16.4 17.1 17.5 16.1 0 113 119 123 125 125 126 122 183 15.7 15.9 0 0 0.7 0.6 0.6 0.6 0.5 0.6 0.6 183 121 eor ...
2021-07-15 3660 1.9 1.9 2.0 17.3 17.0 16.7 16.9 16.3 0 109 115 118 121 122 123 118 159 17.0 17.3 0 0 1.8 1.9 1.8 2.2 1.8 1.8 1.8 159 117 eor ...
2021-07-16 3660 1.4 1.4 0.6 17.1 17.1 17.2 16.8 16.2 0 103 108 113 116 119 121 113 142 16.8 17.1 0 0 1.6 1.3 1.0 1.5 1.4 1.0 1.0 142 113 eor ...
In [10]:
soilmoisture= soil[["BF10" ,"BF20","BF30","BF40","BF50","BF60"]]
soilmoisture60= soil[["BF60"]]
In [11]:
soilmoisture["BFSUM"]= soilmoisture["BF10"]+soilmoisture["BF20"]+soilmoisture["BF30"]+soilmoisture["BF40"]+soilmoisture["BF50"]+soilmoisture["BF60"]
<ipython-input-11-83a619884aa6>:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  soilmoisture["BFSUM"]= soilmoisture["BF10"]+soilmoisture["BF20"]+soilmoisture["BF30"]+soilmoisture["BF40"]+soilmoisture["BF50"]+soilmoisture["BF60"]
In [51]:
soilmoisture60.plot( figsize=(16,5), grid=True, title="soil moisture under grass and sandy loam between 0 and 60 cm depth in % plant useable water"); # .loc["28 June 2021":"18 July 2021"]
In [15]:
soilmoisture["BFSUM"].plot( figsize=(16,5), grid=True, title="soil moisture under grass and sandy loam between 0 and 60 cm depth in % plant useable water"); # .loc["28 June 2021":"18 July 2021"]
In [19]:
soilmoisture["BFSUM"].max()
Out[19]:
731

The soil was at 13-07-2021 up till 60 cm deep full of water.

In [20]:
soilmoisture.loc["28 June 2021":"18 July 2021"].plot( figsize=(16,5), grid=True, title="soil moisture under \
grass and sandy loam between 0 and 60 cm depth in % plant useable water");