Compare commits

..
27 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
4 changed files with 327 additions and 73 deletions
+3 -1
View File
@@ -13,7 +13,9 @@ def main(folder: str = "plots"):
timestamp = timestamp.replace(":", "-") timestamp = timestamp.replace(":", "-")
plot_df = create_plot_df(datetime.datetime.now(), _df_state) plot_df = create_plot_df(datetime.datetime.now(), _df_state)
print(plot_df.sum(1)) print(plot_df.sum(1))
fig.savefig(Path(folder) / f"digital_plot_{timestamp}.png", dpi=300) fig.savefig(
Path(folder) / f"digital_plot_{timestamp}.png", dpi=300, bbox_inches="tight"
)
if __name__ == "__main__": if __name__ == "__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)
+15
View File
@@ -16,4 +16,19 @@
{{ image|safe }} {{ image|safe }}
{{ tables|safe }} {{ tables|safe }}
</main> </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> </article>
+145 -36
View File
@@ -11,10 +11,15 @@ import matplotlib.pyplot as plt
import matplotlib.ticker as mtick import matplotlib.ticker as mtick
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from flask import Flask, Markup, 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, get_landesbezirk from download_digital import (
construct_dataframe,
get_bez_data,
get_landesbezirk,
landesbezirk_dict,
)
config = { config = {
"CACHE_TYPE": "FileSystemCache", "CACHE_TYPE": "FileSystemCache",
@@ -23,6 +28,15 @@ config = {
"CACHE_DIR": "cache", "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" os.environ["TZ"] = "Europe/Berlin"
time.tzset() time.tzset()
@@ -32,13 +46,13 @@ 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(bez_data=bez_data[0], special_tag="stud") df = construct_dataframe(bez_data=bez_data[0], special_tag="stud")
df_state = construct_dataframe(bez_data=bez_data[1]) df_state = construct_dataframe(bez_data=bez_data[1])
return df, df_state return df, df_state, datetime.datetime.now()
def create_plot_df( def create_plot_df(
@@ -68,11 +82,12 @@ def create_plot_df(
data_dict[key] = df["Digitale Befragung"] data_dict[key] = df["Digitale Befragung"]
df = pd.DataFrame(data=data_dict).T df = pd.DataFrame(data=data_dict).T
max_date = df.index.max()
df.index = df.index.astype("datetime64[ns]") + pd.DateOffset(hours=10) df.index = df.index.astype("datetime64[ns]") + pd.DateOffset(hours=10)
df = df.reindex( df = df.reindex(
pd.date_range(start="2023-08-15", end=curr_datetime) + pd.DateOffset(hours=10) pd.date_range(start="2023-08-15", end=max_date) + pd.DateOffset(hours=10)
) )
if current_df is not None: if current_df is not None:
@@ -90,21 +105,22 @@ def create_plot_df(
def plot( def plot(
curr_datetime,
df: pd.DataFrame, df: pd.DataFrame,
annotate_current: bool = False, annotate_current: bool = False,
total_targets: tuple[int, ...] = (1500, 2500, 3500), total_targets: tuple[int, ...] = (1500, 2500, 3500),
alpha: float | None = None, alpha: float | None = None,
landesbez_str: str | None = None, landesbez_str: str | None = None,
fix_lims: bool = True,
max_shading_date=None,
) -> str: ) -> str:
fig = plt.figure(dpi=300) fig = plt.figure(dpi=300, figsize=(8.5, 5))
# fill weekends target_time = pd.Timestamp("2023-10-01")
max_date = curr_datetime + datetime.timedelta(days=1) plt.axvline(x=target_time, color="tab:green", linestyle=":")
days = pd.date_range(start="2023-08-14", end=max_date)
for idx, day in enumerate(days[:-1]): if fix_lims:
if day.weekday() >= 5: for total_target in total_targets:
plt.gca().axvspan(days[idx], days[idx + 1], alpha=0.2, color="gray") plt.axhline(y=total_target, color="#48a9be", linestyle="--")
for bez in landesbez_str: for bez in landesbez_str:
series = df.sum(axis=1) if bez is None else df[bez] series = df.sum(axis=1) if bez is None else df[bez]
@@ -125,7 +141,7 @@ def plot(
ls="--", ls="--",
marker="o", marker="o",
lw=1, lw=1,
color="#e4004e" if bez is None else None, color="#e4004e" if bez is None or not fix_lims else None,
markersize=4, markersize=4,
label=bez if bez is not None else "Bundesweit", label=bez if bez is not None else "Bundesweit",
) )
@@ -152,6 +168,7 @@ def plot(
plt.title("Teilnahme an Digitaler Beschäftigtenbefragung") plt.title("Teilnahme an Digitaler Beschäftigtenbefragung")
plt.ylabel("# Teilnahmen") plt.ylabel("# Teilnahmen")
if fix_lims:
max_val = df.sum(axis=1).max().item() max_val = df.sum(axis=1).max().item()
nearest_target = np.array(total_targets, dtype=np.float32) - max_val nearest_target = np.array(total_targets, dtype=np.float32) - max_val
@@ -159,7 +176,7 @@ def plot(
idx = np.argmin(nearest_target) idx = np.argmin(nearest_target)
ceil_val = max(max_val, total_targets[idx]) ceil_val = max(max_val, total_targets[idx])
plt.ylim(0, ceil_val * 1.025) plt.ylim(0, ceil_val * 1.04)
plt.legend() plt.legend()
# use timezone offset to center tick labels # use timezone offset to center tick labels
@@ -184,8 +201,18 @@ def plot(
sec_ax.set_ylabel("# Teilnahmen [% Erfolg]") sec_ax.set_ylabel("# Teilnahmen [% Erfolg]")
sec_ax.yaxis.set_major_formatter(mtick.PercentFormatter()) sec_ax.yaxis.set_major_formatter(mtick.PercentFormatter())
for total_target in total_targets: xlim = plt.xlim()
plt.axhline(y=total_target, color="#48a9be", linestyle="--")
# 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() plt.tight_layout()
@@ -195,10 +222,12 @@ def plot(
def create_fig( def create_fig(
url: str = "https://beschaeftigtenbefragung.verdi.de/", url: str = "https://beschaeftigtenbefragung.verdi.de/",
importance_factor: float = 1.0, importance_factor: float = 1.0,
landesbez_strs: list[str | None] | None = None,
fix_lims: bool = True,
): ):
curr_datetime = datetime.datetime.now() curr_datetime = datetime.datetime.now()
try: try:
df, df_state = get_tables(url) df, df_state, curr_datetime = get_tables(url)
df = df.sort_values( df = df.sort_values(
["Digitale Befragung", "Bundesland", "Bezirk"], ["Digitale Befragung", "Bundesland", "Bezirk"],
@@ -225,11 +254,12 @@ def create_fig(
{"Digitale Befragung": "Int32"} {"Digitale Befragung": "Int32"}
) )
plot_df = create_plot_df(curr_datetime) plot_df = create_plot_df(curr_datetime, df_state)
annotate_current = False annotate_current = False
timestamp = Markup(f'<font color="red">{key} 10:00:00</font>') timestamp = Markup(f'<font color="red">{key} 10:00:00</font>')
total = plot_df.loc[curr_datetime].sum() total = plot_df.loc[curr_datetime].sum()
if landesbez_strs is None:
landesbez_strs = [None] + [ landesbez_strs = [None] + [
bez bez
for bez in plot_df.columns for bez in plot_df.columns
@@ -237,10 +267,10 @@ def create_fig(
] ]
return ( return (
plot( plot(
curr_datetime,
plot_df, plot_df,
annotate_current=annotate_current, annotate_current=annotate_current,
landesbez_str=landesbez_strs, landesbez_str=landesbez_strs,
fix_lims=fix_lims,
), ),
df, df,
df_state, df_state,
@@ -257,12 +287,41 @@ def convert_fig_to_svg(fig: plt.Figure) -> str:
return imgdata.read() return imgdata.read()
@app.route("/") def _print_as_html(
@cache.cached(query_string=True) df: pd.DataFrame,
def tables(): output_str: list[str],
def _print_as_html(df: pd.DataFrame, total: int | None = None) -> None: df_state: pd.DataFrame | None = None,
dropna: bool = True,
) -> list[str]:
df = df.astype({"Digitale Befragung": "Int32"}) 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() df = df.dropna()
with pd.option_context("display.max_rows", None): with pd.option_context("display.max_rows", None):
table = df.to_html( table = df.to_html(
index_names=False, index_names=False,
@@ -284,16 +343,19 @@ def tables():
" </tr>", " </tr>",
] ]
) )
if total: if total and (diff := total - df["Digitale Befragung"].sum()):
tfoot.extend([ tfoot.append(" <tr>")
" <tr>", num_missing = missing_df["Digitale Befragung"].sum()
" <td>Weitere Bezirke</td>", 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></td>") tfoot.append(" <td></td>")
tfoot.extend( tfoot.extend(
[ [
f" <td>{total - df['Digitale Befragung'].sum()}</td>", f" <td>{diff}</td>",
" </tr>", " </tr>",
] ]
) )
@@ -304,26 +366,37 @@ def tables():
output_str.append(table[: idx - 1]) output_str.append(table[: idx - 1])
output_str.append(tfoot) output_str.append(tfoot)
output_str.append(table[idx:]) 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") importance_factor = request.args.get("importance")
if not importance_factor: if not importance_factor:
importance_factor = 1.0 importance_factor = 1.0
else: else:
importance_factor = float(importance_factor) importance_factor = float(importance_factor)
output_str = [] fig, df, df_state, timestamp = create_fig(landesbez_strs=[state], fix_lims=False)
fig, df, df_state, timestamp = create_fig(importance_factor=importance_factor)
svg_string = convert_fig_to_svg(fig) svg_string = convert_fig_to_svg(fig)
plt.close() plt.close()
_print_as_html(df_state)
df["Bundesland"] = df.index.map(get_landesbezirk) df["Bundesland"] = df.index.map(get_landesbezirk)
df = df.rename(columns={"Bundesland": "Landesbezirk"}) df = df.rename(columns={"Bundesland": "Landesbezirk"})
_print_as_html(df, total=df_state['Digitale Befragung'].sum()) 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",
@@ -333,5 +406,41 @@ def tables():
) )
@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()