feat(#168): Über-Zeit-Drift im Stimmverhalten-Tab
Stimm-Index pro Fraktion über Quartale. Linien-Chart pro Fraktion, Lücken bei Quartalen mit n<3 (Ja UND Nein). Macht sichtbar, ob sich die Gemeinwohl-Affinität einer Fraktion innerhalb der Wahlperiode verschiebt. - `_quarter_for(datum)` Helper: ISO-Datum → "YYYY-Qn". - `aggregate_stimm_index_zeitreihe()` analog zu pro_wert/pro_gruppe, aber nach Quartal-Bucket statt Achse. - `GET /api/auswertungen/stimm-index-zeitreihe?parteien=CDU,SPD,...` - 4. Sub-Section im Stimmverhalten-Tab: Multi-Linien-Chart mit Partei-Farben (CDU schwarz, SPD rot, GRÜNE grün, FDP gelb, AfD blau, LINKE pink, BSW lila, SSW navy, BVB-FW orange). Bei aktueller Sparse-Datenmenge (35 Assessments × 4 Quartale) ist der Chart heute meist leer — Infrastruktur ist ready, fuellt sich automatisch mit Issue #44 Batch-Bewertung. Tests: 10 neue (4 _quarter_for, 6 aggregate). Suite jetzt 1005 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
79003d6056
commit
1e381d23ab
@ -614,6 +614,106 @@ def aggregate_stimm_index_pro_wert(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _quarter_for(datum: str) -> Optional[str]:
|
||||||
|
"""ISO-Datum zu Quartal-Bucket "YYYY-Qn" (z.B. "2024-Q1" für Q1/2024)."""
|
||||||
|
if not datum or len(datum) < 7:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
year = int(datum[:4])
|
||||||
|
month = int(datum[5:7])
|
||||||
|
if not 1 <= month <= 12:
|
||||||
|
return None
|
||||||
|
q = (month - 1) // 3 + 1
|
||||||
|
return f"{year}-Q{q}"
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_stimm_index_zeitreihe(
|
||||||
|
parteien: Optional[list[str]] = None,
|
||||||
|
filter_bl: Optional[str] = None,
|
||||||
|
filter_wp: Optional[str] = None,
|
||||||
|
exclude_antragsteller: bool = True,
|
||||||
|
min_n_per_bucket: int = 3,
|
||||||
|
db_path: Optional[Path] = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Stimm-Index ueber die Zeit, gebuckt nach Quartal — pro Fraktion eine
|
||||||
|
Linie. Macht regionale Drift sichtbar (#168).
|
||||||
|
|
||||||
|
Pro (Fraktion, Quartal) ein stimm_index analog Aussage 1. Buckets mit
|
||||||
|
n_ja < min_n_per_bucket ODER n_nein < min_n_per_bucket bekommen
|
||||||
|
stimm_index=null (im Chart als Lücke).
|
||||||
|
|
||||||
|
`parteien=None` → alle Fraktionen mit ≥1 Datenpunkt; sonst nur die
|
||||||
|
angegebenen.
|
||||||
|
"""
|
||||||
|
rows = _load_assessments_with_votes(filter_bl, filter_wp, db_path)
|
||||||
|
|
||||||
|
ja: defaultdict[tuple[str, str], list[float]] = defaultdict(list)
|
||||||
|
nein: defaultdict[tuple[str, str], list[float]] = defaultdict(list)
|
||||||
|
buckets_seen: set[str] = set()
|
||||||
|
parteien_seen: set[str] = set()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
bucket = _quarter_for(row["datum"])
|
||||||
|
if not bucket:
|
||||||
|
continue
|
||||||
|
buckets_seen.add(bucket)
|
||||||
|
skip = row["antragsteller"] if exclude_antragsteller else set()
|
||||||
|
score = row["gwoe_score"]
|
||||||
|
for f in row["ja"] - skip:
|
||||||
|
parteien_seen.add(f)
|
||||||
|
ja[(f, bucket)].append(score)
|
||||||
|
for f in row["nein"] - skip:
|
||||||
|
parteien_seen.add(f)
|
||||||
|
nein[(f, bucket)].append(score)
|
||||||
|
|
||||||
|
parteien_filter = (
|
||||||
|
set(parteien) if parteien else parteien_seen
|
||||||
|
)
|
||||||
|
parteien_out = sorted(parteien_seen & parteien_filter)
|
||||||
|
buckets_sorted = sorted(buckets_seen)
|
||||||
|
|
||||||
|
series: dict[str, list[Optional[float]]] = {}
|
||||||
|
detail: dict[str, dict[str, dict]] = {}
|
||||||
|
for partei in parteien_out:
|
||||||
|
line = []
|
||||||
|
partei_detail = {}
|
||||||
|
for b in buckets_sorted:
|
||||||
|
n_ja = len(ja[(partei, b)])
|
||||||
|
n_nein = len(nein[(partei, b)])
|
||||||
|
avg_ja = _avg(ja[(partei, b)])
|
||||||
|
avg_nein = _avg(nein[(partei, b)])
|
||||||
|
ausreichend = (n_ja >= min_n_per_bucket
|
||||||
|
and n_nein >= min_n_per_bucket)
|
||||||
|
idx = (round(avg_ja - avg_nein, 2)
|
||||||
|
if avg_ja is not None and avg_nein is not None
|
||||||
|
and ausreichend else None)
|
||||||
|
line.append(idx)
|
||||||
|
partei_detail[b] = {
|
||||||
|
"stimm_index": idx,
|
||||||
|
"n_ja": n_ja,
|
||||||
|
"n_nein": n_nein,
|
||||||
|
"ausreichend": ausreichend,
|
||||||
|
}
|
||||||
|
series[partei] = line
|
||||||
|
detail[partei] = partei_detail
|
||||||
|
|
||||||
|
return {
|
||||||
|
"buckets": buckets_sorted,
|
||||||
|
"fraktionen": parteien_out,
|
||||||
|
"series": series,
|
||||||
|
"detail": detail,
|
||||||
|
"n_assessments_matched": len({r["drucksache"] for r in rows}),
|
||||||
|
"filter": {
|
||||||
|
"bundesland": filter_bl,
|
||||||
|
"wahlperiode": filter_wp,
|
||||||
|
"exclude_antragsteller": exclude_antragsteller,
|
||||||
|
"min_n_per_bucket": min_n_per_bucket,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def aggregate_stimm_index_pro_gruppe(
|
def aggregate_stimm_index_pro_gruppe(
|
||||||
filter_bl: Optional[str] = None,
|
filter_bl: Optional[str] = None,
|
||||||
filter_wp: Optional[str] = None,
|
filter_wp: Optional[str] = None,
|
||||||
|
|||||||
21
app/main.py
21
app/main.py
@ -2370,6 +2370,27 @@ async def auswertungen_stimm_index_cross_bl(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/auswertungen/stimm-index-zeitreihe")
|
||||||
|
async def auswertungen_stimm_index_zeitreihe(
|
||||||
|
bundesland: Optional[str] = None,
|
||||||
|
wahlperiode: Optional[str] = None,
|
||||||
|
parteien: Optional[str] = None, # comma-separated
|
||||||
|
exclude_antragsteller: bool = True,
|
||||||
|
min_n_per_bucket: int = 3,
|
||||||
|
):
|
||||||
|
"""Stimm-Index ueber Zeit (Quartal × Fraktion) — Drift im Stimmverhalten
|
||||||
|
waehrend der Wahlperiode (#168)."""
|
||||||
|
from .auswertungen import aggregate_stimm_index_zeitreihe
|
||||||
|
parteien_list = parteien.split(",") if parteien else None
|
||||||
|
return aggregate_stimm_index_zeitreihe(
|
||||||
|
parteien=parteien_list,
|
||||||
|
filter_bl=bundesland,
|
||||||
|
filter_wp=wahlperiode,
|
||||||
|
exclude_antragsteller=exclude_antragsteller,
|
||||||
|
min_n_per_bucket=min_n_per_bucket,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/auswertungen/stimm-index-pro-gruppe")
|
@app.get("/api/auswertungen/stimm-index-pro-gruppe")
|
||||||
async def auswertungen_stimm_index_pro_gruppe(
|
async def auswertungen_stimm_index_pro_gruppe(
|
||||||
bundesland: Optional[str] = None,
|
bundesland: Optional[str] = None,
|
||||||
|
|||||||
@ -291,9 +291,23 @@ table.modal-table th { background: var(--ecg-bg-subtle); font-weight: 700; }
|
|||||||
</div>
|
</div>
|
||||||
<div id="sv-wert-heatmap" class="matrix-wrap"></div>
|
<div id="sv-wert-heatmap" class="matrix-wrap"></div>
|
||||||
|
|
||||||
<!-- Sub 4: Empfehlungs-Konsistenz Bar Chart -->
|
<!-- Sub 3b: Über-Zeit-Drift Line Chart -->
|
||||||
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
|
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
|
||||||
margin:1.5rem 0 0.5rem;">4. Empfehlungs-Konsistenz (gegen GWÖ-Empfehlung)</h3>
|
margin:1.5rem 0 0.5rem;">4. Über-Zeit-Drift (Quartal × Fraktion)</h3>
|
||||||
|
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.7;margin:0 0 0.5rem;">
|
||||||
|
Stimm-Index pro Fraktion über die Quartale der laufenden Wahlperiode.
|
||||||
|
Pro Fraktion eine Linie. Lücken in Quartalen mit zu wenig Daten (n < 3
|
||||||
|
pro Vote-Richtung). Macht sichtbar, ob sich die Gemeinwohl-Affinität
|
||||||
|
einer Fraktion verschiebt.
|
||||||
|
</p>
|
||||||
|
<div class="matrix-wrap" style="padding:14px;">
|
||||||
|
<canvas id="sv-zeitreihe-chart" style="max-height:400px;"></canvas>
|
||||||
|
</div>
|
||||||
|
<div id="sv-zeitreihe-meta" class="meta-line"></div>
|
||||||
|
|
||||||
|
<!-- Sub 5: Empfehlungs-Konsistenz Bar Chart -->
|
||||||
|
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
|
||||||
|
margin:1.5rem 0 0.5rem;">5. Empfehlungs-Konsistenz (gegen GWÖ-Empfehlung)</h3>
|
||||||
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.7;margin:0 0 0.5rem;">
|
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.7;margin:0 0 0.5rem;">
|
||||||
Anteil der Anträge mit GWÖ-Empfehlung „Uneingeschränkt unterstützen" oder
|
Anteil der Anträge mit GWÖ-Empfehlung „Uneingeschränkt unterstützen" oder
|
||||||
„Unterstützen mit Änderungen", bei denen die Fraktion trotzdem
|
„Unterstützen mit Änderungen", bei denen die Fraktion trotzdem
|
||||||
@ -305,9 +319,9 @@ table.modal-table th { background: var(--ecg-bg-subtle); font-weight: 700; }
|
|||||||
</div>
|
</div>
|
||||||
<div id="sv-empfehlung-meta" class="meta-line"></div>
|
<div id="sv-empfehlung-meta" class="meta-line"></div>
|
||||||
|
|
||||||
<!-- Sub 5: Cross-BL Grouped Bar -->
|
<!-- Sub 6: Cross-BL Grouped Bar -->
|
||||||
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
|
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
|
||||||
margin:1.5rem 0 0.5rem;">5. Stimm-Index pro Bundesland (Cross-BL)</h3>
|
margin:1.5rem 0 0.5rem;">6. Stimm-Index pro Bundesland (Cross-BL)</h3>
|
||||||
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.7;margin:0 0 0.5rem;">
|
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.7;margin:0 0 0.5rem;">
|
||||||
Gleiche Fraktion in mehreren Ländern. Macht regionale Drift sichtbar
|
Gleiche Fraktion in mehreren Ländern. Macht regionale Drift sichtbar
|
||||||
— gleiche Partei, unterschiedlicher Stimm-Index? Nur Fraktionen in ≥2 BL
|
— gleiche Partei, unterschiedlicher Stimm-Index? Nur Fraktionen in ≥2 BL
|
||||||
@ -351,7 +365,7 @@ table.modal-table th { background: var(--ecg-bg-subtle); font-weight: 700; }
|
|||||||
{% block body_scripts %}
|
{% block body_scripts %}
|
||||||
<script>
|
<script>
|
||||||
let _tabLoaded = { 'bl-partei': false, 'themen': false, 'stimmverhalten': false };
|
let _tabLoaded = { 'bl-partei': false, 'themen': false, 'stimmverhalten': false };
|
||||||
let _svCharts = { index: null, heuchelei: null, empfehlung: null, crossBl: null };
|
let _svCharts = { index: null, heuchelei: null, empfehlung: null, crossBl: null, zeitreihe: null };
|
||||||
let _svMatrixAxis = 'werte'; // 'werte' or 'gruppen'
|
let _svMatrixAxis = 'werte'; // 'werte' or 'gruppen'
|
||||||
|
|
||||||
function setMatrixAxis(axis) {
|
function setMatrixAxis(axis) {
|
||||||
@ -588,6 +602,7 @@ async function loadStimmverhalten() {
|
|||||||
loadStimmIndex(bl, exclude);
|
loadStimmIndex(bl, exclude);
|
||||||
loadHeuchelei(bl);
|
loadHeuchelei(bl);
|
||||||
loadMatrixHeatmap();
|
loadMatrixHeatmap();
|
||||||
|
loadStimmIndexZeitreihe(bl, exclude);
|
||||||
loadEmpfehlungsKonsistenz(bl);
|
loadEmpfehlungsKonsistenz(bl);
|
||||||
loadStimmIndexCrossBl(exclude);
|
loadStimmIndexCrossBl(exclude);
|
||||||
}
|
}
|
||||||
@ -787,6 +802,83 @@ async function loadStimmIndexProWert(bl, exclude) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SV_PARTEI_COLORS = {
|
||||||
|
'CDU': '#1a1a1a', 'CSU': '#1a1a1a',
|
||||||
|
'SPD': '#e3000f',
|
||||||
|
'GRÜNE': '#46962b',
|
||||||
|
'FDP': '#ffed00',
|
||||||
|
'AfD': '#0489db',
|
||||||
|
'LINKE': '#be3075',
|
||||||
|
'BSW': '#7d1f8a',
|
||||||
|
'SSW': '#003d8f',
|
||||||
|
'BVB-FW': '#f7941d',
|
||||||
|
'FREIE WÄHLER': '#f7941d',
|
||||||
|
};
|
||||||
|
|
||||||
|
function svParteiColor(p) { return SV_PARTEI_COLORS[p] || '#888'; }
|
||||||
|
|
||||||
|
async function loadStimmIndexZeitreihe(bl, exclude) {
|
||||||
|
const meta = document.getElementById('sv-zeitreihe-meta');
|
||||||
|
let url = `/api/auswertungen/stimm-index-zeitreihe?exclude_antragsteller=${exclude}&min_n_per_bucket=3`;
|
||||||
|
if (bl) url += '&bundesland=' + encodeURIComponent(bl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(url);
|
||||||
|
const data = await r.json();
|
||||||
|
if (_svCharts.zeitreihe) _svCharts.zeitreihe.destroy();
|
||||||
|
|
||||||
|
const fraktionen = data.fraktionen.filter(p => {
|
||||||
|
// Mindestens 1 Bucket mit ausreichend Daten
|
||||||
|
return data.buckets.some(b => data.detail[p][b].ausreichend);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fraktionen.length || !data.buckets.length) {
|
||||||
|
meta.textContent = `Datenbasis: ${data.n_assessments_matched} Anträge — kein Quartal erreicht das Mindest-N (3 Ja UND 3 Nein pro Fraktion).`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasets = fraktionen.map(p => ({
|
||||||
|
label: p,
|
||||||
|
data: data.series[p],
|
||||||
|
borderColor: svParteiColor(p),
|
||||||
|
backgroundColor: svParteiColor(p) + '33',
|
||||||
|
fill: false,
|
||||||
|
spanGaps: true,
|
||||||
|
tension: 0.2,
|
||||||
|
pointRadius: 4,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ctx = document.getElementById('sv-zeitreihe-chart');
|
||||||
|
_svCharts.zeitreihe = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: { labels: data.buckets, datasets: datasets },
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
y: { min: -10, max: 10, title: { display: true, text: 'Stimm-Index (−10..+10)' } },
|
||||||
|
x: { title: { display: true, text: 'Quartal' } },
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
afterLabel: (ctx) => {
|
||||||
|
const p = ctx.dataset.label;
|
||||||
|
const b = data.buckets[ctx.dataIndex];
|
||||||
|
const d = data.detail[p][b];
|
||||||
|
return `n_ja=${d.n_ja}, n_nein=${d.n_nein}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
meta.textContent = `Datenbasis: ${data.n_assessments_matched} Anträge über ${data.buckets.length} Quartale. Mindest-N pro Bucket: 3 Ja UND 3 Nein.`;
|
||||||
|
} catch (e) {
|
||||||
|
meta.textContent = 'Fehler: ' + e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStimmIndexProGruppe(bl, exclude) {
|
async function loadStimmIndexProGruppe(bl, exclude) {
|
||||||
const wrap = document.getElementById('sv-wert-heatmap');
|
const wrap = document.getElementById('sv-wert-heatmap');
|
||||||
let url = `/api/auswertungen/stimm-index-pro-gruppe?exclude_antragsteller=${exclude}&min_n=3`;
|
let url = `/api/auswertungen/stimm-index-pro-gruppe?exclude_antragsteller=${exclude}&min_n=3`;
|
||||||
|
|||||||
@ -19,8 +19,10 @@ from app.auswertungen import (
|
|||||||
aggregate_stimm_index_cross_bl,
|
aggregate_stimm_index_cross_bl,
|
||||||
aggregate_stimm_index_pro_gruppe,
|
aggregate_stimm_index_pro_gruppe,
|
||||||
aggregate_stimm_index_pro_wert,
|
aggregate_stimm_index_pro_wert,
|
||||||
|
aggregate_stimm_index_zeitreihe,
|
||||||
export_stimmverhalten_csv,
|
export_stimmverhalten_csv,
|
||||||
_gruppen_score_for_assessment,
|
_gruppen_score_for_assessment,
|
||||||
|
_quarter_for,
|
||||||
_wert_score_for_assessment,
|
_wert_score_for_assessment,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -438,6 +440,71 @@ class TestAggregateProGruppe:
|
|||||||
assert set(wert_out["werte"]).isdisjoint(set(gruppe_out["gruppen"]))
|
assert set(wert_out["werte"]).isdisjoint(set(gruppe_out["gruppen"]))
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# aggregate_stimm_index_zeitreihe (#168)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestQuarterFor:
|
||||||
|
def test_q1(self):
|
||||||
|
assert _quarter_for("2024-01-15") == "2024-Q1"
|
||||||
|
assert _quarter_for("2024-03-31") == "2024-Q1"
|
||||||
|
|
||||||
|
def test_q2(self):
|
||||||
|
assert _quarter_for("2024-04-01") == "2024-Q2"
|
||||||
|
assert _quarter_for("2024-06-30") == "2024-Q2"
|
||||||
|
|
||||||
|
def test_q3_q4(self):
|
||||||
|
assert _quarter_for("2024-07-15") == "2024-Q3"
|
||||||
|
assert _quarter_for("2024-12-31") == "2024-Q4"
|
||||||
|
|
||||||
|
def test_invalid(self):
|
||||||
|
assert _quarter_for("") is None
|
||||||
|
assert _quarter_for("garbage") is None
|
||||||
|
assert _quarter_for("2024-13-01") is None # invalid month
|
||||||
|
|
||||||
|
|
||||||
|
class TestAggregateZeitreihe:
|
||||||
|
def test_structure(self, sample_db):
|
||||||
|
out = aggregate_stimm_index_zeitreihe(db_path=sample_db, min_n_per_bucket=1)
|
||||||
|
assert "buckets" in out
|
||||||
|
assert "fraktionen" in out
|
||||||
|
assert "series" in out
|
||||||
|
assert "detail" in out
|
||||||
|
assert "n_assessments_matched" in out
|
||||||
|
|
||||||
|
def test_buckets_sorted(self, sample_db):
|
||||||
|
out = aggregate_stimm_index_zeitreihe(db_path=sample_db, min_n_per_bucket=1)
|
||||||
|
assert out["buckets"] == sorted(out["buckets"])
|
||||||
|
|
||||||
|
def test_series_alignment(self, sample_db):
|
||||||
|
"""Pro Fraktion: series-Liste muss exakt so lang sein wie buckets."""
|
||||||
|
out = aggregate_stimm_index_zeitreihe(db_path=sample_db, min_n_per_bucket=1)
|
||||||
|
for partei in out["fraktionen"]:
|
||||||
|
assert len(out["series"][partei]) == len(out["buckets"])
|
||||||
|
|
||||||
|
def test_min_n_per_bucket(self, sample_db):
|
||||||
|
"""Mit hohem min_n_per_bucket wird stimm_index meist None."""
|
||||||
|
out = aggregate_stimm_index_zeitreihe(
|
||||||
|
db_path=sample_db, min_n_per_bucket=100,
|
||||||
|
)
|
||||||
|
for partei in out["fraktionen"]:
|
||||||
|
for idx in out["series"][partei]:
|
||||||
|
assert idx is None
|
||||||
|
|
||||||
|
def test_filter_by_parteien(self, sample_db):
|
||||||
|
"""Wenn parteien-Filter gesetzt, nur diese in fraktionen."""
|
||||||
|
out = aggregate_stimm_index_zeitreihe(
|
||||||
|
db_path=sample_db, parteien=["AfD"], min_n_per_bucket=1,
|
||||||
|
)
|
||||||
|
assert out["fraktionen"] == ["AfD"]
|
||||||
|
|
||||||
|
def test_empty_db(self, tmp_path):
|
||||||
|
out = aggregate_stimm_index_zeitreihe(db_path=tmp_path / "missing.db")
|
||||||
|
assert out["buckets"] == []
|
||||||
|
assert out["fraktionen"] == []
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# aggregate_stimm_index_cross_bl
|
# aggregate_stimm_index_cross_bl
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user