Vergleichbare stündliche Beobachtungen aufbauen

PythonBeginner
Jetzt üben

Einführung

Eine Gemeinschaftsausstellung braucht einen sinnvollen Vergleich ihrer Öffnungszeitfenster. Eine höhere Gesamtzahl an Besuchern kann schlicht mehr Gelegenheiten zur Erfassung bedeuten. Du vergleichst zunächst zwei gleich lange beobachtete Zeitfenster und erstellst dann eine wiederverwendbare Analyse, die offene Stunden, vorhandene Aufzeichnungen und weiterhin unbekannte Werte unterscheidet.

Dieses Lab setzt Python-Grundlagen, Einlesen, Filtern und Gruppieren mit pandas sowie Matplotlib-Balkendiagramme voraus. Python und die Bibliotheken sind in der WebIDE vorbereitet. Alle Zählwerte sind synthetisch; eine Zeile steht für eine Stunde, nicht für die Identität einer Person. Die Dauer ist eine Autorenschätzung einschließlich Lesen und gewöhnlicher Fehler. Du erstellst echte CSV- und PNG-Dateien und bewahrst sämtliche Originaleingaben.

Zwei gleich lange beobachtete Zeitfenster vergleichen

In diesem Schritt berechnest du eine Rate von Hand, bevor du das erste Diagramm zeichnest. Verwende im WebIDE-Tab das Terminal unten. Ist es ausgeblendet, öffne oben links das Menü und wähle Terminal → New Terminal. Befehle laufen in diesem Terminal; Dateien erscheinen links im Explorer.

Wechsle in den vorbereiteten Arbeitsbereich. cd ändert das Arbeitsverzeichnis dieses Terminals, nicht das anderer Terminals. pwd zeigt das aktuelle Verzeichnis. Meldet ein späterer Befehl eine fehlende Datei, kehre hierher zurück und prüfe den Namen mit ls.

cd /home/labex/project/opening-hours
pwd
/home/labex/project/opening-hours

cat zeigt eine Textdatei an, ohne sie zu ändern. Diese CSV enthält je Zeitfenster zwei Beobachtungen von jeweils einer Stunde.

cat data/equal.csv
slot,visitors
morning,6
morning,10
evening,8
evening,12

Am Morgen wurden in zwei aufgezeichneten Stunden 16 Besucher gezählt: 16 / 2 = 8 Besucher pro Stunde. Am Abend sind es 20 in zwei Stunden: 20 / 2 = 10. Die Nenner sind gleich, daher spricht sowohl der Gesamtvergleich als auch der Stundenvergleich für den Abend. Eine Rate behält ihre Einheit: Besucher pro aufgezeichneter Stunde.

Erstelle das folgende kleine Skript. In der Shell ersetzt > die angegebene Datei. <<'PY' beginnt ein Here-Dokument: Alle Zeilen bis zu einer Zeile, die nur PY enthält, werden wörtlich gespeichert. Füge den gesamten Block einschließlich der letzten Zeile ein. Dadurch entsteht die Datei sofort; du musst sie nicht im Editor speichern. agg benennt die Ergebnisspalten, size zählt die beobachteten Stunden. index=False verhindert, dass der pandas-Zeilenindex in die CSV geschrieben wird. Mit Agg kann Matplotlib eine PNG-Datei speichern, ohne ein eigenes Fenster zu öffnen.

cat > equal.py <<'PY'
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

data = pd.read_csv('data/equal.csv')
summary = data.groupby('slot').agg(visitors=('visitors', 'sum'), hours=('visitors', 'size'))
summary['rate'] = summary['visitors'] / summary['hours']
summary.to_csv('results/equal-summary.csv')
summary['rate'].plot.bar(color=['#256b5d', '#b35828'], rot=0)
plt.ylabel('Visitors per recorded hour')
plt.title('Equal recording opportunities: two hours each')
plt.tight_layout()
plt.savefig('results/equal.png', dpi=150)
plt.close()
print(summary)
PY
python3 equal.py
         visitors  hours  rate
slot
evening        20      2  10.0
morning        16      2   8.0

Die Zeilen sind nach Zeitfensternamen sortiert; führende Leerzeichen in der Anzeige können variieren. Öffne im Explorer opening-hours/results/equal.png, um das gespeicherte Diagramm zu sehen. Die beiden senkrechten Balken sollen 10 und 8 Besucher pro aufgezeichneter Stunde zeigen. Behalte jeden Zeitfensternamen an seiner horizontalen Achsenmarkierung und die vertikale Beschriftung Visitors per recorded hour bei; Farben, Schriftarten, Bildgröße und Reihenfolge sind frei. Das PNG ist deine tatsächliche Ausgabe, keine vorbereitete Zielgrafik. Das Diagramm muss deterministisch sein: Bei unveränderter Eingabe muss das aktuelle Skript dieselben Bildpixel erzeugen. Vermeide zufällige Stile und Zeitstempel. PNG-Metadaten dürfen abweichen; eigene Farben, Schriftarten und Größen bleiben erlaubt.

Bei einem gewöhnlichen Dateinamenfehler gilt zunächst: Python meldet die fehlende Datei und hält an; deine Arbeit wurde nicht zurückgesetzt. Der folgende Fehler ist beabsichtigt:

python3 missing.py

Du solltest can't open file und No such file or directory sehen. Liste zur Wiederherstellung die Dateien auf und starte erneut mit dem richtigen Namen.

ls
python3 equal.py

Selbstkontrolle: Beide Zeitfenster verwenden zwei beobachtete Stunden. Du hast noch nicht angenommen, dass alle späteren Daten die gleichen Aufzeichnungsmöglichkeiten besitzen.

Jede geplante Stunde erhalten

In diesem Schritt ordnest du Besucherzahlen ihren geplanten Stunden zu. Arbeite im WebIDE-Terminal unter /home/labex/project/opening-hours weiter. Jede Planzeile ist eine Stunde an einem Datum in einem Zeitfenster. open=1 bedeutet geplante Öffnung, open=0 Schließung. Das Paar date und slot bildet einen zusammengesetzten Schlüssel: Keine der beiden Spalten identifiziert allein eine Zeile. Die Datumswerte verwenden YYYY-MM-DD; alle Datumswerte in diesen Dateien sind gültige Kalenderdaten.

cat data/plan.csv
cat data/counts.csv

Die Zähldatei hat absichtlich eine andere Reihenfolge. Der Zählwert 0 ist eine echte Beobachtung ohne Besucher. Fehlt ein Zählwert für eine geplante offene Stunde, fehlt die Aufzeichnung; das bedeutet nicht null Besucher. Geschlossene Stunden haben keine Zählzeile. Jeder Zählwert muss zu einer offenen Planzeile passen; beide Dateien benötigen vollständige, eindeutige zusammengesetzte Schlüssel. Entferne keine Duplikate und erfinde keine Werte, um einen ungültigen Schlüssel zu verbergen.

Eine Linksverknüpfung (left merge) erhält jede Zeile der linken Tabelle, also des Plans. Sie sucht Zählwerte anhand beider Schlüsselspalten, unabhängig von den Zeilenpositionen. Ohne Treffer entsteht der fehlende pandas-Wert NaN. Prüfe vor der Verknüpfung, dass die Schlüssel vollständig sind: pandas kann fehlende Schlüssel miteinander verknüpfen, was keine echte Stunde identifiziert. duplicated(keys) erkennt wiederholte Schlüsselpaare, und validate='one_to_one' verlangt die Ablehnung doppelter Beziehungen.

Das Skript verwendet .loc[condition, column], um nur passenden Zeilen Werte zuzuweisen. .eq(0) und .eq(1) vergleichen jeden Wert mit null oder eins; & verknüpft Bedingungen und notna() unterscheidet einen vorhandenen Zählwert von einem fehlenden Wert. Zuerst wirkt die Maske für geschlossene Stunden, danach die für offene Stunden mit Aufzeichnung. Deshalb gilt ein echter Nullwert als beobachtet.

Ersetze die vorbereitete observe.py durch das vollständige Skript unten. sys.argv[1] und [2] lesen die beiden Pfadargumente nach dem Skriptnamen. Ein Path mit / bildet einen Dateipfad. mkdir(parents=True, exist_ok=True) legt das Ausgabeverzeichnis bei Bedarf an, damit dasselbe Programm ein anderes Eingabeverzeichnis analysieren kann. Die beiden Eingabe-CSV-Dateien bleiben unverändert und werden nur gelesen.

cat > observe.py <<'PY'
import sys
from pathlib import Path
import pandas as pd

source = Path(sys.argv[1])
output = Path(sys.argv[2])
plan = pd.read_csv(source / 'plan.csv')
counts = pd.read_csv(source / 'counts.csv')
keys = ['date', 'slot']
if plan[keys].isna().any().any() or counts[keys].isna().any().any():
    raise ValueError('Every date and slot must be present')
if plan.duplicated(keys).any() or counts.duplicated(keys).any():
    raise ValueError('Every date/slot key must be unique')
if not plan['open'].isin([0, 1]).all():
    raise ValueError('open must be 0 or 1')
if counts['visitors'].isna().any() or (counts['visitors'] < 0).any():
    raise ValueError('Recorded counts must be nonnegative')
known = counts.merge(plan, on=keys, how='left', validate='one_to_one')
if known['open'].isna().any() or (known['open'] != 1).any():
    raise ValueError('Counts must match an open planned hour')
hours = plan.merge(counts, on=keys, how='left', validate='one_to_one')
hours['status'] = 'missing'
hours.loc[hours['open'].eq(0), 'status'] = 'closed'
hours.loc[hours['open'].eq(1) & hours['visitors'].notna(), 'status'] = 'observed'
hours['valid_hour'] = hours['status'].eq('observed').astype(int)
output.mkdir(parents=True, exist_ok=True)
hours.sort_values(keys).to_csv(output / 'hours.csv', index=False)
print(hours[['date', 'slot', 'visitors', 'status']].head())
PY

Die erste Maske markiert geschlossene Stunden. Die zweite markiert offene Stunden mit Zählwert als beobachtet, einschließlich echter Nullwerte. & verbindet zwei elementweise Bedingungen, notna() prüft das Vorhandensein eines Wertes. valid_hour wandelt die Beobachtungsbedingung in 1 oder 0 um; ihre Summe zählt die nutzbaren Beobachtungsstunden. Kein fehlender Zählwert wird durch null ersetzt.

python3 observe.py data results
head -n 10 results/hours.csv

head -n 10 zeigt die Kopfzeile und die ersten neun Datenzeilen. Die gespeicherte CSV enthält date,slot,open,visitors,status,valid_hour und ist nach dem zusammengesetzten Schlüssel sortiert. Prüfe sie auch im Explorer: quiet hat erfasste Nullwerte mit observed und valid_hour=1; unlogged hat leere Besucherzahlen mit missing und valid_hour=0; closed hat leere Zählwerte mit closed. Je nach Status bedeutet leer unbekannt oder nicht anwendbar, niemals eine vermutete Null.

Selbstkontrolle: Jede Planzeile erscheint genau einmal in hours.csv, und jeder Zählwert bleibt seinem Datum und Zeitfenster zugeordnet. Bei einem ValueError lies, welche Beziehung ungültig ist. Vergleiche dein Skript mit diesem Block und den unveränderten Quellen; korrigiere das Skript und starte es erneut. Das Terminal musst du nicht neu öffnen.

Raten und Aufzeichnungsabdeckung berechnen

In diesem Schritt fasst du jedes Zeitfenster zusammen, ohne die Bedeutung eines unbekannten Zählwertes zu ändern. Verwende dasselbe WebIDE-Terminal und denselben Arbeitsbereich weiter.

Bei evening enthält der Plan vier offene Stunden, von denen drei jeweils 12 Besucher erfasst haben. Die Summe ist 36, die beobachteten Stunden sind 3, die Stundenrate ist 36 / 3 = 12 und die Abdeckung ist 3 / 4 = 0.75: 75% der geplanten offenen Stunden wurden aufgezeichnet. Fehlende Stunden gehen nicht in den Nenner der Rate ein, verringern aber die Abdeckung. morning hat 40 Besucher in vier beobachteten Stunden: Die höhere Summe hat die niedrigere Rate 10.

Bei quiet ergeben vier aufgezeichnete Nullwerte Summe 0 und Rate 0 bei vollständiger Abdeckung. Bei unlogged wurde keine Stunde beobachtet: Die Abdeckung ist 0, aber Summe und Rate bleiben leer. Bei closed sind die geplanten offenen Stunden 0: Abdeckung und Rate sind undefiniert und bleiben leer. Ein undefinierter Wert darf später nicht als Ratenbalken der Höhe null erscheinen.

Hänge den folgenden Block einmal an observe.py an. >> hängt an, statt zu ersetzen. Er verwendet die zuvor erstellten Variablen hours und output. sum(min_count=1) liefert eine fehlende Summe, wenn alle Werte fehlen; die Standardsumme würde ohne gültige Beobachtungen null ergeben. replace(0, float('nan')) macht Nullnenner vor der Division ausdrücklich zu fehlenden Werten. Eine tatsächlich beobachtete Null im Zähler bleibt dabei erhalten.

cat >> observe.py <<'PY'

grouped = hours.groupby('slot')
summary = grouped.agg(planned_hours=('open', 'sum'), observed_hours=('valid_hour', 'sum'))
summary['visitors'] = grouped['visitors'].sum(min_count=1)
summary['coverage'] = summary['observed_hours'] / summary['planned_hours'].replace(0, float('nan'))
summary['rate'] = summary['visitors'] / summary['observed_hours'].replace(0, float('nan'))
summary.to_csv(output / 'summary.csv')
print(summary)
PY
python3 observe.py data results
cat results/summary.csv
slot,planned_hours,observed_hours,visitors,coverage,rate
afternoon,3,3,36.0,1.0,12.0
closed,0,0,,,
evening,4,3,36.0,0.75,12.0
morning,4,4,40.0,1.0,10.0
quiet,4,4,0.0,1.0,0.0
short,4,1,20.0,0.25,20.0
sparse,5,3,27.0,0.6,9.0
unlogged,4,0,,0.0,

Die Zahlendarstellung in der CSV, etwa 36 statt 36.0, darf variieren, ohne das Ergebnis zu ändern. Pflichtspalten und Bedeutung bleiben gleich. short hat die höchste beobachtete Rate, aber nur eine beobachtete Stunde. sparse besitzt drei Beobachtungen, deckt jedoch nur 60% der geplanten Stunden ab. Deshalb muss vor einer Empfehlung die Qualität der Evidenz betrachtet werden; dieses Lab hat noch keines dieser Zeitfenster zum Gewinner erklärt.

Öffne results/summary.csv im Explorer. Bewahre data/plan.csv, data/counts.csv, data/equal.csv und deine Skripte auf. Der wiederverwendbare Aufruf lautet python3 observe.py INPUT_DIRECTORY OUTPUT_DIRECTORY: Beide CSV-Dateien werden aus der Eingabe gelesen und hours.csv sowie summary.csv in die Ausgabe geschrieben. Gleichwertige Berechnungen und eine andere Zeilenreihenfolge sind erlaubt, solange alle Schlüssel und Zahlenbeziehungen erhalten bleiben. Hast du beim Anhängen versehentlich das Skript ersetzt, erstelle das Skript aus Schritt 2 erneut, hänge diesen Block einmal an und führe es wieder aus.

Selbstkontrolle: Ein unbeobachtetes Zeitfenster und ein wirklich beobachtetes Zeitfenster ohne Besucher besitzen unterschiedliche Summen, Nenner und Raten. Das nächste Lab nutzt diese Unterschiede, um zu entscheiden, wann die Evidenz für einen Vergleich ausreicht.

Zusammenfassung

Du hast ein Diagramm mit gleichen Beobachtungsmöglichkeiten erstellt, Zählwerte über vollständige zusammengesetzte Schlüssel zugeordnet und CSV-Dateien nach Stunden und Zeitfenstern erzeugt. Geschlossene Stunden, fehlende Aufzeichnungen und echte Nullwerte bleiben verschieden. Die Raten verwenden aufgezeichnete offene Stunden; die Abdeckung zeigt, welcher Teil der geplanten Möglichkeiten tatsächlich erfasst wurde.

Bewahre die wiederverwendbare Analyse und die Originaldaten auf. Diese beschreibenden historischen Raten belegen nicht, was nach einer Verlängerung der Öffnungszeiten passieren würde. In der nächsten unabhängigen Umgebung wendest du vor einer Empfehlung eine ausdrückliche Evidenzregel an.