Compare commits

..
72 Commits
Author SHA1 Message Date
Felix Blanke 196972b90b Fix fallback 2023-12-03 12:57:01 +01:00
Felix Blanke d205625ef4 Update plot 2023-09-28 20:38:48 +02:00
Felix Blanke 6060930208 Tweak plot params 2023-09-20 02:49:26 +02:00
Felix Blanke 0cd5377442 Tweak plot params 2023-09-20 02:46:34 +02:00
Felix Blanke 29459d5386 Add endpoint that only returns the total number 2023-09-19 19:46:38 +02:00
Felix Blanke 4f1835c8f8 Format 2023-09-19 19:46:24 +02:00
Felix Blanke 32bd83f054 Return curr datetime at table creation 2023-09-19 19:46:01 +02:00
Felix Blanke 2cbf2af0de Generalize inference of missing bezirk data 2023-09-14 11:34:27 +02:00
Felix Blanke 13d47be9c1 Infer Bezirk value from total if only one entry is missing 2023-09-14 11:05:57 +02:00
Felix Blanke 14e314822d Add number of further districts 2023-09-13 16:26:51 +02:00
Felix Blanke 3953d6d2ff Add current datapoint & format 2023-09-12 16:42:56 +02:00
Felix Blanke a6325468ca Reduce whitespace around ims and use green for regression line 2023-09-07 13:44:40 +02:00
Felix Blanke 76d890980c Format 2023-09-07 13:11:32 +02:00
Felix Blanke 06c68c5167 Add regression script 2023-09-07 13:10:42 +02:00
Felix Blanke 387b976f81 Use max index instead of curr_date 2023-09-07 12:37:50 +02:00
Felix Blanke 9b17bafb97 Draw vlines first 2023-09-07 12:37:25 +02:00
Felix Blanke d2d157c479 Simplify plot 2023-09-07 12:37:02 +02:00
Felix Blanke 40c30fdde8 Format 2023-09-07 10:12:39 +02:00
Felix Blanke a0daed66ad Fix shading 2023-09-01 23:19:48 +02:00
Felix Blanke 3cae0c6c10 Only show diff if not 0 2023-09-01 15:00:50 +02:00
Felix Blanke 51670ea193 Add links to html template 2023-09-01 14:59:09 +02:00
Felix Blanke 504c075ccf Add Niedersachsen-Bremen as an abbrev 2023-09-01 14:50:22 +02:00
Felix Blanke d3c424d90a Add route for single states 2023-09-01 14:47:40 +02:00
Felix Blanke aaf4ee1863 Add option to not drop NA 2023-09-01 14:46:58 +02:00
Felix Blanke f9c14e442b Make create_fig more general 2023-09-01 14:46:37 +02:00
Felix Blanke ef8cab6330 Add option to not fix lims in plot 2023-09-01 14:46:10 +02:00
Felix Blanke dbf477ed17 Refactor 2023-09-01 14:24:45 +02:00
Felix Blanke 0292db36e2 Show the Landesbezirk instead of the Bundesland 2023-09-01 14:19:56 +02:00
Felix Blanke 6e1247177b Remove NA entries from Bezirk table and show delta 2023-09-01 14:19:31 +02:00
Felix Blanke 594ddbd740 Handle case with no arg 2023-09-01 11:02:24 +02:00
Felix Blanke 162a77d31a format 2023-09-01 10:57:25 +02:00
Felix Blanke c75e11204a Add importance factor 2023-09-01 10:57:16 +02:00
Felix Blanke bd804f1464 Make ylim rendering dynamic 2023-08-31 11:30:48 +02:00
Felix Blanke 87f54c6fef Add further target values 2023-08-31 11:30:13 +02:00
Felix Blanke cc7ba64dd3 Add time series logging to plot.py 2023-08-31 11:17:01 +02:00
Felix Blanke 976ddc6c23 Add plots to gitignore 2023-08-29 15:40:02 +02:00
Felix Blanke 196b3f7576 Add plotting code 2023-08-29 15:39:36 +02:00
Felix Blanke bee447b334 Encapsulate data creation for wsgi 2023-08-29 15:39:21 +02:00
Felix Blanke 3593f2ecd9 Close figures 2023-08-29 10:12:04 +02:00
Felix Blanke dbce381a71 Add possibility for multi-plots 2023-08-29 01:26:40 +02:00
Felix Blanke bb214df990 Update gitignore 2023-08-29 00:39:37 +02:00
Felix Blanke 092a1d7417 Format 2023-08-29 00:27:55 +02:00
Felix Blanke 014217604c Fix timezone 2023-08-29 00:27:26 +02:00
Felix Blanke c5f6067e8b Encapsulate svg conversion 2023-08-28 17:12:06 +02:00
Felix Blanke 45647def39 Do not plot lines if no datapoint in set 2023-08-28 17:08:50 +02:00
Felix Blanke 11a4cf4248 Allow filtering for landesbezirk 2023-08-28 17:08:03 +02:00
Felix Blanke dc80671295 Group by Landesbezirk 2023-08-28 16:56:01 +02:00
Felix Blanke 046fce6bb0 Print occurred errors 2023-08-28 12:12:58 +02:00
Felix Blanke 204195ac06 Allow multiple target lines 2023-08-28 12:12:42 +02:00
Felix Blanke 6f29bdc6da Fix index col for read data 2023-08-28 12:12:08 +02:00
Felix Blanke 7aca691596 Encapsulate df creation 2023-08-28 12:11:35 +02:00
Felix Blanke ae6beafa3d Simplify download script 2023-08-28 12:10:05 +02:00
Felix Blanke 3bb4f432e4 Fix empty td tags 2023-08-28 01:04:35 +02:00
Felix Blanke 4e43916fb1 If no dataframe was yet created for the current day remove the entry for plotting purposes 2023-08-28 00:12:01 +02:00
Felix Blanke 598a22ad87 Reuse current datetime 2023-08-28 00:11:14 +02:00
Felix Blanke 71a5ded906 Remove unneeded param 2023-08-28 00:10:46 +02:00
Felix Blanke c4f3fb2120 Make skript handle case of verdi page failing gracefully 2023-08-27 23:54:16 +02:00
Felix Blanke dcbec17064 Fix wrong name 2023-08-27 23:30:43 +02:00
Felix Blanke 2412a45d1d Add missing footer table row 2023-08-27 23:30:25 +02:00
Felix Blanke 2916db7858 Cleanup cache params 2023-08-27 23:21:56 +02:00
Felix Blanke 0eb3bb6b91 Export plot to svg instead of png 2023-08-27 23:20:16 +02:00
Felix Blanke 0e16891961 Refactor CSS 2023-08-27 23:10:34 +02:00
Felix Blanke ddbb5e3505 Add important remark 2023-08-27 19:06:34 +02:00
Felix Blanke f1471e21a5 Set German Locale explicitely 2023-08-27 18:59:37 +02:00
Felix Blanke e571c6178e Center xtick labels 2023-08-27 18:58:35 +02:00
Felix Blanke 19dd8c5f92 Add line plot 2023-08-27 18:57:29 +02:00
Felix Blanke 95ba5a57a7 Add img to base html 2023-08-27 16:22:22 +02:00
Felix Blanke f7f2861682 Add bar plot to page 2023-08-27 16:22:06 +02:00
Felix Blanke f24b1f9297 Remove image tag 2023-08-27 16:21:51 +02:00
Felix Blanke d73de33cb8 Format 2023-08-27 16:19:47 +02:00
Felix Blanke 29f33b2b9b Use flex layout 2023-08-27 16:17:35 +02:00
Felix Blanke 4a99d42852 Use File System Cache 2023-08-27 11:48:58 +02:00
7 changed files with 617 additions and 72 deletions
+3 -1
View File
@@ -1,2 +1,4 @@
data.html plots
.venv
cache
__pycache__ __pycache__
+29 -19
View File
@@ -71,6 +71,24 @@ bundesland_dict = {
} }
landesbezirk_dict = {
"100": "Nord",
"200": "Niedersachsen-Bremen",
"300": "Berlin-Brandenburg",
"400": "Nordrhein-Westfalen",
"500": "Rheinland-Pfalz-Saarland",
"600": "Hessen",
"700": "Sachsen, Sachsen-Anhalt, Thüringen",
"800": "Bayern",
"900": "Baden-Württemberg",
"1000": "Hamburg",
}
def get_landesbezirk(id: str):
return landesbezirk_dict[str((int(id) // 100) * 100)]
def get_bez_data( def get_bez_data(
tags: list[str], url: str = "https://beschaeftigtenbefragung.verdi.de/" tags: list[str], url: str = "https://beschaeftigtenbefragung.verdi.de/"
) -> list[dict]: ) -> list[dict]:
@@ -90,22 +108,22 @@ def get_bez_data(
def construct_dataframe( def construct_dataframe(
bez_data: dict[str, dict], bez_data: dict[str, dict],
grouped: bool = False,
special_tag: str | None = None, special_tag: str | None = None,
no_processing: bool = False,
): ):
data = {} data = {}
if not no_processing:
first_key = next(iter(bez_data.keys()))
if first_key in landesbezirk_dict:
data["Landesbezirk"] = pd.Series(
[v["name"] for v in bez_data.values()], index=list(bez_data.keys())
)
else:
data["Bundesland"] = pd.Series( data["Bundesland"] = pd.Series(
[bundesland_dict[k] for k in bez_data], index=list(bez_data.keys()) [bundesland_dict[k] for k in bez_data], index=list(bez_data.keys())
) )
data["Bezirk"] = pd.Series( data["Bezirk"] = pd.Series(
[v["name"] for v in bez_data.values()], index=list(bez_data.keys()) [v["name"] for v in bez_data.values()], index=list(bez_data.keys())
) )
else:
data["Landesbezirk"] = pd.Series(
[v["name"] for v in bez_data.values()], index=list(bez_data.keys())
)
tot_col_data = [] tot_col_data = []
tot_col_index = [] tot_col_index = []
@@ -121,15 +139,7 @@ def construct_dataframe(
tot_col_index.append(k) tot_col_index.append(k)
data["Digitale Befragung"] = pd.Series(tot_col_data, index=tot_col_index) data["Digitale Befragung"] = pd.Series(tot_col_data, index=tot_col_index)
df = pd.DataFrame(data=data) return pd.DataFrame(data=data).astype({"Digitale Befragung": "Int32"})
df = df.astype({"Digitale Befragung": "Int32"})
if grouped and no_processing:
raise ValueError
elif grouped:
df = df.groupby("Bundesland", as_index=False)[["Digitale Befragung"]].sum()
return df
def main( def main(
@@ -138,7 +148,6 @@ def main(
dry_run: bool = False, dry_run: bool = False,
grouped: bool = False, grouped: bool = False,
special_tag: str | None = None, special_tag: str | None = None,
no_processing: bool = False,
folder: str = "data", folder: str = "data",
name: str = "data", name: str = "data",
sheet_name: str = "digital", sheet_name: str = "digital",
@@ -146,11 +155,12 @@ def main(
bez_data = get_bez_data([tag], url)[0] bez_data = get_bez_data([tag], url)[0]
df = construct_dataframe( df = construct_dataframe(
bez_data=bez_data, bez_data=bez_data,
grouped=grouped,
special_tag=special_tag, special_tag=special_tag,
no_processing=no_processing,
) )
if grouped:
df = df.groupby("Bundesland", as_index=False)[["Digitale Befragung"]].sum()
if dry_run: if dry_run:
print(df) print(df)
else: else:
+22
View File
@@ -0,0 +1,22 @@
import datetime
from pathlib import Path
import fire
import matplotlib.pyplot as plt
from wsgi import create_fig, create_plot_df
def main(folder: str = "plots"):
fig, _df, _df_state, timestamp = create_fig()
timestamp = timestamp.replace(" ", "_")
timestamp = timestamp.replace(":", "-")
plot_df = create_plot_df(datetime.datetime.now(), _df_state)
print(plot_df.sum(1))
fig.savefig(
Path(folder) / f"digital_plot_{timestamp}.png", dpi=300, bbox_inches="tight"
)
if __name__ == "__main__":
fire.Fire(main)
+128
View File
@@ -0,0 +1,128 @@
import datetime
from pathlib import Path
import fire
import matplotlib
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import numpy as np
import pandas as pd
import scipy
from wsgi import create_fig, create_plot_df, get_tables, plot
def create_dfs(url: str = "https://beschaeftigtenbefragung.verdi.de/"):
try:
df, df_state, curr_datetime = get_tables(url)
df = df.sort_values(
["Digitale Befragung", "Bundesland", "Bezirk"],
ascending=[False, True, True],
)
df_state = df_state.sort_values("Landesbezirk")
plot_df = create_plot_df(curr_datetime, df_state)
except Exception as e:
print(e)
last_file = sorted(Path("data").iterdir())[-1]
key = last_file.name[:10]
with (Path("data") / f"{key}_data.ods").open("rb") as ff:
df = pd.read_excel(ff, sheet_name="digital", index_col=0).astype(
{"Digitale Befragung": "Int32"}
)
with (Path("data") / f"{key}_state_data.ods").open("rb") as ff:
df_state = pd.read_excel(ff, sheet_name="digital", index_col=0).astype(
{"Digitale Befragung": "Int32"}
)
plot_df = create_plot_df(None, None)
return df, df_state, plot_df
def main():
df, df_state, plot_df = create_dfs()
plot(plot_df, landesbez_str=[None], max_shading_date="2023-10-02")
plt.gcf().set_size_inches(10, 5)
target_time = pd.Timestamp("2023-10-01")
xlim = plt.xlim()
plt.xlim(xlim[0], pd.Timestamp("2023-10-02"))
plt.ylim(0, 3500 * 1.025)
data = plot_df.dropna().sum(1)
data = data.iloc[3:]
casted_timepoints = data.index.to_numpy().astype(np.int64)
reg = scipy.stats.linregress(casted_timepoints, data)
print(f"Regression R^2: {reg.rvalue**2:.6f}")
date_range = pd.date_range(start="2023-08-21 10:00:00", end=target_time)
date_range = date_range.to_series(index=np.arange(len(date_range)))
date_range.loc[len(date_range)] = target_time
regression_curve = lambda x: reg.intercept + reg.slope * x.astype(np.int64)
vals = regression_curve(date_range.to_numpy())
print(f"Projizierte Teilnahme am {target_time}: {vals[-1]:.2f}")
now = pd.Timestamp.now()
print(
f"Projizierte Teilnahme jetzt: {regression_curve(pd.Series([now]).to_numpy()).item():.2f}"
)
print()
for target in [1500, 2500, 3500]:
target_reached_date = (target - reg.intercept) / reg.slope
print(
f"Ziel {target} erreicht am {pd.Timestamp(target_reached_date).strftime('%Y-%m-%d %X')}"
)
num_skipped_days = 2
x = date_range.to_numpy().astype(np.int64)
curr_time = x[data.index.argmax() + num_skipped_days]
delta = 3500 - data[-1]
target_line = data[-1] + delta / (x[-1] - curr_time) * (
x[data.index.argmax() + num_skipped_days :] - curr_time
)
plt.plot(
date_range,
vals,
label=f"Lineare Regression ($R^2={reg.rvalue**2:.3f}$)",
color="tab:green",
zorder=1,
)
plt.plot(
date_range[data.index.argmax() + num_skipped_days :],
target_line,
label="Ziellinie",
color="tab:orange",
linestyle=":",
zorder=1,
)
# plt.gca().relim() # make sure all the data fits
# plt.gca().autoscale() # auto-scale
plt.xlabel("Zeit in Tagen ab dem 15.08.")
plt.axvline(x=target_time, color="tab:red", linestyle="--")
plt.legend()
plt.gca().xaxis.set_major_locator(matplotlib.ticker.NullLocator())
plt.gca().xaxis.set_major_locator(matplotlib.ticker.NullLocator())
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%d.%m."))
plt.gca().set_xticks([target_time])
plt.title("Projektion Teilnahme an Digitaler Beschäftigtenbefragung")
plt.savefig("plots/regression.png", bbox_inches="tight", dpi=300)
if __name__ == "__main__":
fire.Fire(main)
+9 -4
View File
@@ -1,6 +1,13 @@
main {
display: flex;
flex-flow: row wrap;
justify-content: space-around;
gap: 1em;
align-items: flex-start;
}
p { p {
font-family: sans-serif; font-family: sans-serif;
margin: 10px;
} }
a { a {
@@ -10,13 +17,11 @@ a {
.dataframe { .dataframe {
border-collapse: collapse; border-collapse: collapse;
margin: 25px 0;
font-size: 0.9em; font-size: 0.9em;
font-family: sans-serif; font-family: sans-serif;
min-width: 400px; min-width: 400px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
float: left; flex-grow: 1;
margin: 10px;
} }
.dataframe thead tr { .dataframe thead tr {
+24 -2
View File
@@ -5,8 +5,30 @@
<title>Digitale Beschäftigtenbefragung</title> <title>Digitale Beschäftigtenbefragung</title>
<article>
<header>
<p>Daten abgerufen: {{ timestamp }}</p> <p>Daten abgerufen: {{ timestamp }}</p>
<p><i>Hinweis:</i> Die Daten entstammen der <a href="https://zusammen-geht-mehr.verdi.de/beschaeftigtenbefragung">Karte der digitalen Beschäftigtenbefragung</a> und sind dort nur für die ver.di-Bezirke auf Bezirksebene aufgelöst, bei denen TVStud auch als Schwerpunkt angegeben ist.</p> <p><i>Hinweis:</i> Die Daten entstammen der <a href="https://zusammen-geht-mehr.verdi.de/beschaeftigtenbefragung">Karte der digitalen Beschäftigtenbefragung</a> und sind dort nur für die ver.di-Bezirke auf Bezirksebene aufgelöst, bei denen TVStud auch als Schwerpunkt angegeben ist.</p>
</header>
{{ tables|safe }} <main>
{{ image|safe }}
{{ tables|safe }}
</main>
<footer>
<p>Ergebnisse einzelner Landesbezirke:
<a href="/">Bundesweit</a> |
<a href="/BaWü">Baden-Württemberg</a> |
<a href="/Bayern">Bayern</a> |
<a href="/BBR">Berlin-Brandenburg</a> |
<a href="/Hamburg">Hamburg</a> |
<a href="/NDS">Niedersachsen-Bremen</a> |
<a href="/Nord">Nord</a> |
<a href="/NRW">NRW</a> |
<a href="/RLP">Rheinland-Pfalz-Saarland</a> |
<a href="/SAT">Sachsen, Sachsen-Anhalt, Thüringen</a>
</p>
</footer>
</article>
+402 -46
View File
@@ -1,90 +1,446 @@
import datetime import datetime
import io
import locale
import os
import time
from itertools import chain
from pathlib import Path
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import numpy as np
import pandas as pd import pandas as pd
from flask import Flask, render_template, request from flask import Flask, Markup, abort, render_template, request
from flask_caching import Cache from flask_caching import Cache
from download_digital import construct_dataframe, get_bez_data from download_digital import (
construct_dataframe,
get_bez_data,
get_landesbezirk,
landesbezirk_dict,
)
config = { config = {
"CACHE_TYPE": "SimpleCache", # Flask-Caching related configs "CACHE_TYPE": "FileSystemCache",
"CACHE_DEFAULT_TIMEOUT": 300, "CACHE_DEFAULT_TIMEOUT": 300,
"CACHE_THRESHOLD": 50,
"CACHE_DIR": "cache",
} }
abbrev_dict = {
"BBR": "Berlin-Brandenburg",
"BaWü": "Baden-Württemberg",
"NDS": "Niedersachsen-Bremen",
"NRW": "Nordrhein-Westfalen",
"RLP": "Rheinland-Pfalz-Saarland",
"SAT": "Sachsen, Sachsen-Anhalt, Thüringen",
}
os.environ["TZ"] = "Europe/Berlin"
time.tzset()
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
app = Flask(__name__) app = Flask(__name__)
app.config.from_mapping(config) app.config.from_mapping(config)
cache = Cache(app) cache = Cache(app)
def get_tables(url: str) -> tuple[pd.DataFrame, pd.DataFrame]: def get_tables(url: str) -> tuple[pd.DataFrame, pd.DataFrame, datetime.datetime]:
bez_data = get_bez_data(["bez_data_0", "bez_data_2"], url) bez_data = get_bez_data(["bez_data_0", "bez_data_2"], url)
df = construct_dataframe( df = construct_dataframe(bez_data=bez_data[0], special_tag="stud")
bez_data=bez_data[0], df_state = construct_dataframe(bez_data=bez_data[1])
grouped=False,
special_tag="stud",
)
df_state = construct_dataframe( return df, df_state, datetime.datetime.now()
bez_data=bez_data[1], grouped=False, no_processing=True
)
return df, df_state
@app.route("/") def create_plot_df(
@cache.cached(timeout=50) curr_datetime,
def tables( current_df: pd.DataFrame | None,
url: str = "https://beschaeftigtenbefragung.verdi.de/", data_folder: str = "data",
): sheet_name: str = "digital",
df, df_state = get_tables(url) ) -> pd.DataFrame:
data_dict = {}
df = df.sort_values( ## Important: If multiple results are stored for the same date
["Digitale Befragung", "Bundesland", "Bezirk"], ascending=[False, True, True] ## the last is used. So this relies on the Landesbezirk data
) ## to be stored with a filename that is lexigraphically larger
## than the single district results.
df_state = df_state.sort_values("Landesbezirk") for f in sorted(Path(data_folder).iterdir()):
with f.open("rb") as ff:
df = pd.read_excel(ff, sheet_name=sheet_name, index_col=0)
output_str = [] if "Landesbezirk" not in df.columns:
df["Landesbezirk"] = df.index.map(get_landesbezirk)
def _print_as_html(df: pd.DataFrame):
df = df.astype({"Digitale Befragung": "Int32"}) df = df.astype({"Digitale Befragung": "Int32"})
with pd.option_context("display.max_rows", None): df = df.groupby("Landesbezirk")[["Digitale Befragung"]].sum()
table = df.to_html(
index_names=False, key = f.name[:10]
justify="left", data_dict[key] = df["Digitale Befragung"]
index=False,
classes="sortable dataframe", df = pd.DataFrame(data=data_dict).T
max_date = df.index.max()
df.index = df.index.astype("datetime64[ns]") + pd.DateOffset(hours=10)
df = df.reindex(
pd.date_range(start="2023-08-15", end=max_date) + pd.DateOffset(hours=10)
)
if current_df is not None:
if "Landesbezirk" not in current_df.columns:
current_df["Landesbezirk"] = current_df.index.map(get_landesbezirk)
current_df = current_df.astype({"Digitale Befragung": "Int32"})
current_df = current_df.groupby("Landesbezirk")[["Digitale Befragung"]].sum()
df.loc[curr_datetime] = current_df["Digitale Befragung"]
if pd.isna(df.loc[df.index.max()][0]):
df = df.drop([df.index.max()])
return df
def plot(
df: pd.DataFrame,
annotate_current: bool = False,
total_targets: tuple[int, ...] = (1500, 2500, 3500),
alpha: float | None = None,
landesbez_str: str | None = None,
fix_lims: bool = True,
max_shading_date=None,
) -> str:
fig = plt.figure(dpi=300, figsize=(8.5, 5))
target_time = pd.Timestamp("2023-10-01")
plt.axvline(x=target_time, color="tab:green", linestyle=":")
if fix_lims:
for total_target in total_targets:
plt.axhline(y=total_target, color="#48a9be", linestyle="--")
for bez in landesbez_str:
series = df.sum(axis=1) if bez is None else df[bez]
plot_df = series.to_frame("Digitale Befragung").replace(0, np.nan)
plot_df = plot_df.astype({"Digitale Befragung": "float32"})
if not pd.isna(plot_df).all().item():
if alpha is not None:
plt.fill_between(
plot_df.dropna().index,
plot_df.dropna()["Digitale Befragung"],
color="#e4004e",
alpha=alpha,
)
(line,) = plt.plot(
plot_df.dropna().index,
plot_df.dropna()["Digitale Befragung"],
ls="--",
marker="o",
lw=1,
color="#e4004e" if bez is None or not fix_lims else None,
markersize=4,
label=bez if bez is not None else "Bundesweit",
) )
tfoot = [ if annotate_current and bez is None:
" <tfoot>", plt.annotate(
" <td>Gesamt</td>", "Jetzt",
(
plot_df.dropna().index[-1],
plot_df.dropna()["Digitale Befragung"][-1] * 1.03,
),
fontsize=8,
ha="center",
)
plt.plot(
plot_df.index,
plot_df["Digitale Befragung"],
lw=1.5,
color=line.get_color(),
# label=bez,
)
plt.title("Teilnahme an Digitaler Beschäftigtenbefragung")
plt.ylabel("# Teilnahmen")
if fix_lims:
max_val = df.sum(axis=1).max().item()
nearest_target = np.array(total_targets, dtype=np.float32) - max_val
nearest_target[nearest_target <= 0] = np.inf
idx = np.argmin(nearest_target)
ceil_val = max(max_val, total_targets[idx])
plt.ylim(0, ceil_val * 1.04)
plt.legend()
# use timezone offset to center tick labels
plt.gca().xaxis.set_major_locator(
mdates.WeekdayLocator([mdates.TU], tz="Etc/GMT+12")
)
plt.gca().xaxis.set_minor_locator(mdates.DayLocator())
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%a %d.%m."))
plt.grid(True, which="major", axis="y")
plt.grid(True, which="minor", axis="x")
plt.gca().tick_params("x", length=0, which="major")
def val_to_perc(val):
return 100 * val / total_targets[0]
def perc_to_val(perc):
return perc * total_targets[0] / 100
sec_ax = plt.gca().secondary_yaxis("right", functions=(val_to_perc, perc_to_val))
sec_ax.set_ylabel("# Teilnahmen [% Erfolg]")
sec_ax.yaxis.set_major_formatter(mtick.PercentFormatter())
xlim = plt.xlim()
# fill weekends
if max_shading_date is None:
max_shading_date = df.index.max() + datetime.timedelta(days=4)
days = pd.date_range(start="2023-08-14", end=max_shading_date)
for idx, day in enumerate(days[:-1]):
if day.weekday() >= 5:
plt.gca().axvspan(days[idx], days[idx + 1], alpha=0.2, color="gray")
# reset xlim
plt.xlim((xlim[0], pd.Timestamp("2023-10-02")))
plt.tight_layout()
return fig
def create_fig(
url: str = "https://beschaeftigtenbefragung.verdi.de/",
importance_factor: float = 1.0,
landesbez_strs: list[str | None] | None = None,
fix_lims: bool = True,
):
curr_datetime = datetime.datetime.now()
try:
df, df_state, curr_datetime = get_tables(url)
df = df.sort_values(
["Digitale Befragung", "Bundesland", "Bezirk"],
ascending=[False, True, True],
)
df_state = df_state.sort_values("Landesbezirk")
plot_df = create_plot_df(curr_datetime, df_state)
annotate_current = True
timestamp = curr_datetime.strftime("%Y-%m-%d %H:%M:%S")
except Exception as e:
print(e)
last_file = sorted(Path("data").iterdir())[-1]
key = last_file.name[:10]
with (Path("data") / f"{key}_data.ods").open("rb") as ff:
df = pd.read_excel(ff, sheet_name="digital", index_col=0).astype(
{"Digitale Befragung": "Int32"}
)
with (Path("data") / f"{key}_state_data.ods").open("rb") as ff:
df_state = pd.read_excel(ff, sheet_name="digital", index_col=0).astype(
{"Digitale Befragung": "Int32"}
)
plot_df = create_plot_df(curr_datetime, df_state)
annotate_current = False
timestamp = Markup(f'<font color="red">{key} 10:00:00</font>')
total = plot_df.loc[curr_datetime].sum()
if landesbez_strs is None:
landesbez_strs = [None] + [
bez
for bez in plot_df.columns
if plot_df.loc[curr_datetime][bez] >= importance_factor * total
]
return (
plot(
plot_df,
annotate_current=annotate_current,
landesbez_str=landesbez_strs,
fix_lims=fix_lims,
),
df,
df_state,
timestamp,
)
def convert_fig_to_svg(fig: plt.Figure) -> str:
# Convert plot to SVG image
imgdata = io.StringIO()
fig.savefig(imgdata, format="svg")
imgdata.seek(0) # rewind the data
return imgdata.read()
def _print_as_html(
df: pd.DataFrame,
output_str: list[str],
df_state: pd.DataFrame | None = None,
dropna: bool = True,
) -> list[str]:
df = df.astype({"Digitale Befragung": "Int32"})
missing_df = (
df[["Digitale Befragung"]]
.isna()
.join(df[["Landesbezirk"]])
.groupby("Landesbezirk")
.sum()
)
total = df_state["Digitale Befragung"].sum() if df_state is not None else None
if df_state is not None:
for idx, row in missing_df.loc[
missing_df["Digitale Befragung"] == 1
].iterrows():
df_tmp = df.loc[df["Landesbezirk"] == idx]
df_state_tmp = df_state.loc[df_state["Landesbezirk"] == idx]
missing_idx = df_tmp.loc[df_tmp.isna().any(axis=1)].iloc[0].name
df["Digitale Befragung"].loc[missing_idx] = (
df_state_tmp["Digitale Befragung"].sum()
- df_tmp["Digitale Befragung"].sum()
)
df = df.sort_values(
["Digitale Befragung", "Landesbezirk", "Bezirk"],
ascending=[False, True, True],
)
if dropna:
df = df.dropna()
with pd.option_context("display.max_rows", None):
table = df.to_html(
index_names=False,
justify="left",
index=False,
classes="sortable dataframe",
)
tfoot = [
" <tfoot>",
" <tr>",
" <td>Gesamt</td>",
]
for i in range(len(df.columns) - 2):
tfoot.append(" <td></td>")
tfoot.extend(
[
f" <td>{df['Digitale Befragung'].sum()}</td>",
" </tr>",
] ]
)
if total and (diff := total - df["Digitale Befragung"].sum()):
tfoot.append(" <tr>")
num_missing = missing_df["Digitale Befragung"].sum()
tfoot.append(
f" <td>Weitere Bezirke ({num_missing})</td>"
if num_missing
else f" <td>Weitere Bezirke</td>"
)
for i in range(len(df.columns) - 2): for i in range(len(df.columns) - 2):
tfoot.append(" <td/>") tfoot.append(" <td></td>")
tfoot.extend( tfoot.extend(
[ [
f" <td>{df['Digitale Befragung'].sum()}</td>", f" <td>{diff}</td>",
" </tr>", " </tr>",
" </tfoot>",
] ]
) )
tfoot = "\n".join(tfoot) tfoot.append(" </tfoot>")
idx = table.index("</table>")
output_str.append(table[: idx - 1])
output_str.append(tfoot)
output_str.append(table[idx:])
_print_as_html(df_state) tfoot = "\n".join(tfoot)
_print_as_html(df) idx = table.index("</table>")
output_str.append(table[: idx - 1])
output_str.append(tfoot)
output_str.append(table[idx:])
return output_str
@app.route("/<state>")
@cache.cached(query_string=True)
def state_dashboard(state: str):
if state in abbrev_dict:
state = abbrev_dict[state]
if state not in landesbezirk_dict.values():
abort(404)
importance_factor = request.args.get("importance")
if not importance_factor:
importance_factor = 1.0
else:
importance_factor = float(importance_factor)
fig, df, df_state, timestamp = create_fig(landesbez_strs=[state], fix_lims=False)
svg_string = convert_fig_to_svg(fig)
plt.close()
df["Bundesland"] = df.index.map(get_landesbezirk)
df = df.rename(columns={"Bundesland": "Landesbezirk"})
df_state = df_state.loc[df_state["Landesbezirk"] == state]
df = df.loc[df["Landesbezirk"] == state]
output_str = []
output_str = _print_as_html(df_state, output_str, dropna=False)
output_str = _print_as_html(df, output_str, df_state=df_state, dropna=False)
return render_template( return render_template(
"base.html", "base.html",
tables="\n".join(output_str), tables="\n".join(output_str),
timestamp=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), timestamp=timestamp,
image=svg_string,
) )
@app.route("/")
@cache.cached(query_string=True)
def dashboard():
importance_factor = request.args.get("importance")
if not importance_factor:
importance_factor = 1.0
else:
importance_factor = float(importance_factor)
fig, df, df_state, timestamp = create_fig(importance_factor=importance_factor)
svg_string = convert_fig_to_svg(fig)
plt.close()
df["Bundesland"] = df.index.map(get_landesbezirk)
df = df.rename(columns={"Bundesland": "Landesbezirk"})
output_str = []
output_str = _print_as_html(df_state, output_str, dropna=False)
output_str = _print_as_html(df, output_str, df_state)
return render_template(
"base.html",
tables="\n".join(output_str),
timestamp=timestamp,
image=svg_string,
)
@app.route("/total")
@cache.cached(timeout=60)
def total_result(url: str = "https://beschaeftigtenbefragung.verdi.de/"):
df, df_state, curr_datetime = get_tables(url)
total = df_state["Digitale Befragung"].sum().item()
return f"{total}"
if __name__ == "__main__": if __name__ == "__main__":
app.run() app.run()