5  Übung Hooke’sches Gesetz

Häufig liegen Sensordaten in mehreren Dateien vor. Mögliche Gründe dafür können sein, dass die Messung

Im Ordner ‘01-daten/hooke’ liegen mehrere txt-Dateien mit Messdaten zur Federausdehnung. In diesem Kapitel sollen Sie das bisher Gelernte anwenden und die folgenden Fragen beantworten.

  1. Liegen ungültige Messungen vor?
  2. Welche Werte können für die Federkonstanten ermittelt werden?
  3. Wurden die Messungen mit der gleichen Feder (oder mit Federn mit gleicher Federkonstante) durchgeführt, wenn als Vertrauenswahrscheinlichkeit 90 % bzw. 95 % angenommen werden soll?

Im Abschnitt Kapitel 5.1 finden Sie Hinweise und im Abschnitt Kapitel 5.2 eine Musterlösung zum Einlesen der Dateien. Anschließend sollen Sie die Aufgabenstellung eigenständig bearbeiten. In Abschnitt Kapitel 5.3 finden Sie eine Musterlösung für die Aufbereitung der Daten. In Kapitel 5.4 die Musterlösung zur Bestimmung der Federkonstanten.

Hinweis: Je nach gewähltem Vorgehen können sich unterschiedliche Ergebnisse ergeben (etwa bei der Bewertung von und dem Umgang mit Extremwerten / Ausreißern).

5.1 Dateien einlesen

Für das Einlesen der Dateien können Sie das Modul glob verwenden (siehe Methodenbaustein Einlesen strukturierter Datensätze).

Zunächst kann der Funktion glob.glob() das Argument pathname = * übergeben werden. Der Platzhalter * steht für eine beliebige Zeichenfolge (außer Dateipfadelemente wie / oder .), sodass die Namen aller im angegebenen Ordner gespeicherten Dateien ausgelesen werden. Auf diese Weise kann die Anzahl der Dateien und die Dateiendung bestimmt werden, falls dies noch unbekannt ist.

ordnerpfad = '01-daten/hooke'

pfadliste = glob.glob(pathname = '*', root_dir = ordnerpfad, recursive = False)
print(pfadliste)
print(f"Anzahl Dateien: {len(pfadliste)}")
['team_fabi.txt', 'team_die_ahnungslosen.txt', 'team_ma.txt', 'team_kreativkoepfe.txt']
Anzahl Dateien: 4

Mit den Dateipfaden können die Dateien mit Hilfe einer Schleife in eine Liste eingelesen werden. Zunächst werden nur die jeweils ersten 3 Zeilen eingelesen, um einen Eindruck vom Aufbau der Dateien zu erhalten.

for pfad in pfadliste:
  zwischenspeicher = pd.read_csv(filepath_or_buffer = ordnerpfad + '/' + pfad, nrows = 3)
  print(pfad, "\n", zwischenspeicher, "\n", sep = '')
team_fabi.txt
   09:17:54\t110.31 cm\t0
0  09:17:57\t110.29 cm\t0
1  09:18:03\t110.74 cm\t0
2  09:18:06\t109.95 cm\t0

team_die_ahnungslosen.txt
   11:03:23\t109.66 cm\t0
0  11:03:23\t109.62 cm\t0
1  11:03:23\t109.73 cm\t0
2  11:03:23\t109.55 cm\t0

team_ma.txt
   10:09:38\t109.26 cm\t0
0  10:09:41\t109.26 cm\t0
1  10:09:44\t109.28 cm\t0
2  10:09:47\t109.18 cm\t0

team_kreativkoepfe.txt
   10:33:02\t109.64 cm\t0
0  10:33:05\t109.62 cm\t0
1  10:33:08\t109.64 cm\t0
2  10:33:11\t109.62 cm\t0

Die Dateien beinhalten keine Spaltenbeschriftung und verwenden den Tabulator ‘\t’ als Trennzeichen. Die erste Spalte enthält einen Zeitstempel, die zweite die gemessene Federausdehnung und die dritte (vermutlich) das angehängte Gewicht.

5.2 Aufgabe Dateien einlesen

Lesen Sie die Dateien nun ein. Prüfen Sie dabei:

  • ob die Datentypen korrekt eingelesen werden und
  • auf fehlende Werte.

Sie können:

  1. Jede Datei einzeln einlesen.
  2. Mit dem Modul glob die Dateien automatisch einlesen und jeweils in einem separaten Objekt speichern (Hinweis: Dieses Vorgehen wird im Methodenbaustein Einlesen strukturierter Datensätze gezeigt).
  3. Die Dateien mit dem Modul glob automatisch einlesen und in einer Datei zusammenführen.

Die verschiedenen Möglichkeiten sind mit zunehmend mehr Aufwand beim Programmieren verbunden. Je mehr separate Dateien Sie auswerten möchten, desto mehr Automatisierung ist gefragt. Da bei der Auswertung von Sensordaten häufig zahlreiche Dateien ausgewertet werden müssen, wird in der Musterlösung Variante c) gezeigt.

Tipp 5.1: Schrittweises Vorgehen

Das Einlesen der Dateien wird voraussichtlich der aufwändigste und fehleranfälligste Arbeitsschritt sein. Entwickeln Sie Ihre Lösung Schritt für Schritt. Beginnen Sie mit der Variante a). Wenn Sie die Dateien eingelesen haben, können Sie sich durch die Weiterentwicklung zur Variante b) mit dem Modul glob vertraut machen. Darauf aufbauend können Sie mit der Variante c) die Automatisierung für beliebig viele Dateien umsetzen.

Der erste Versuch, die Dateien einzulesen, scheitert mit einer Fehlermeldung. Die Anweisungen werden deshalb in die Struktur zur Ausnahmebehandlung eingebettet und die verursachende Datei abgefangen. (Sollten mehrere Dateien Fehler erzeugen, müssten die Dateien in einer Liste gespeichert und später - falls möglich - mit einer Schleife weiter behandelt werden.)

hooke = pd.DataFrame(columns = ['Zeit', 'Abstand', 'Gewicht', 'Team']) # ein leerer DataFrame

for pfad in pfadliste:

  try:
    zwischenspeicher = pd.read_csv(filepath_or_buffer = ordnerpfad + '/' + pfad, sep = '\t', names = ['Zeit', 'Abstand', 'Gewicht'])

    # Dateiname als Spalte einfügen
    zwischenspeicher['Team'] = pfad[5:-4] # 'team_' und die Dateiendung abschneiden 

    hooke = pd.concat([hooke, zwischenspeicher], ignore_index = True)
  
  except Exception as error:
    print("Pfad der Problemdatei: ", pfad, error, sep = "\n")
    pfad_problem_datei = pfad

print(hooke.info(), "\n")
print("Erfolgreich einglesen:\n", hooke['Team'].unique(), sep = '')
Pfad der Problemdatei: 
team_ma.txt
Error tokenizing data. C error: Expected 3 fields in line 135, saw 4

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 359 entries, 0 to 358
Data columns (total 4 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   Zeit     355 non-null    object
 1   Abstand  355 non-null    object
 2   Gewicht  355 non-null    object
 3   Team     359 non-null    object
dtypes: object(4)
memory usage: 11.3+ KB
None 

Erfolgreich einglesen:
['fabi' 'die_ahnungslosen' 'kreativkoepfe']

Der Fehlermeldung zufolge besteht Zeile 135 aus 4 statt aus 3 Spalten. Die fehlerverursachende Datei wird deshalb zeilenweise durchlaufen und jede Zeile ausgegeben, die mehr als 3 Einträge hat. Zur Kontrolle werden auch die ersten 5 Zeilen ausgegeben.

# einen leeren DataFrame mit 3 Spalten erstellen
df = pd.DataFrame(data = [], columns = ['Zeit', 'Abstand', 'Gewicht'])

dateiobjekt_problem_datei = open(file = ordnerpfad + '/' + pfad_problem_datei, mode = 'r')

index = 0
for zeile in dateiobjekt_problem_datei:
  try:
    zwischenspeicher = zeile.split(sep = "\t")
    if len(zwischenspeicher) > 3:
      print("Index =", index, ":", zwischenspeicher)
    elif index <= 5:
      print(zeile)
    index += 1
  except Exception as error:
    print(error)

dateiobjekt_problem_datei.close()
10:09:38    109.26 cm   0



10:09:41    109.26 cm   0



10:09:44    109.28 cm   0



Index = 134 : ['10:29:27', '105.49 cm', '300', '\n']

Jede zweite Zeile ist leer. In Zeile 134 wird ein Zeilenumbruch ‘\n’ eingelesen. Die Datei wird deshalb mit einer angepassten Schleife erneut durchlaufen. Aus der betreffenden Zeile wird der zusätzliche Zeilenumbruch ‘\n’ entfernt. Leere Zeilen werden übersprungen. Die korrekten Zeilen werden an den DataFrame hooke angefügt.

dateiobjekt_problem_datei = open(file = ordnerpfad + '/' + pfad_problem_datei, mode = 'r')

for zeile in dateiobjekt_problem_datei:
  try:
    zwischenspeicher = zeile.split(sep = "\t")
    if len(zwischenspeicher) > 3:
      zwischenspeicher = zwischenspeicher[:3]
    elif len(zwischenspeicher) < 3: # leere Zeilen überspringen
      continue
    # Teamnamen als 4. Spalte anfügen
    zwischenspeicher.append(pfad_problem_datei[5:-4]) # 'team_' und die Dateiendung abschneiden 

    hooke.loc[len(hooke)] = pd.Series(zwischenspeicher).values

  except Exception as error:
    print(error)
    print(pd.Series(zwischenspeicher).values)

dateiobjekt_problem_datei.close()

print("Erfolgreich einglesen:\n", hooke['Team'].unique(), "\n", sep = '')
print(hooke.info())
Erfolgreich einglesen:
['fabi' 'die_ahnungslosen' 'kreativkoepfe' 'ma']

<class 'pandas.core.frame.DataFrame'>
Index: 537 entries, 0 to 536
Data columns (total 4 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   Zeit     533 non-null    object
 1   Abstand  533 non-null    object
 2   Gewicht  533 non-null    object
 3   Team     537 non-null    object
dtypes: object(4)
memory usage: 21.0+ KB
None

Anschließend werden zum einen die Zeilen mit Nullwerten betrachtet …

print(hooke.loc[hooke.apply(pd.isna).any(axis = 1), :])
    Zeit Abstand Gewicht              Team
207  NaN     NaN     NaN  die_ahnungslosen
247  NaN     NaN     NaN     kreativkoepfe
267  NaN     NaN     NaN     kreativkoepfe
272  NaN     NaN     NaN     kreativkoepfe

… und entfernt.

hooke.drop(np.where(hooke.apply(pd.isna).any(axis = 1))[0], inplace = True)

Zum anderen werden die Datentypen kontrolliert.

  • Die Zeit kann als string stehen bleiben, da sie für die Auswertung nicht benötigt wird.
  • Der gemessene Abstand ist mit ’ cm’ notiert - diese Zeichenkette wird entfernt. Anschließend sollte die Spalte als numerisch erkannt werden.
  • Das Gewicht sollte numerische Werte enthalten, wird aber als Datentyp object eingelesen und muss weiter untersucht werden.
  • Der Spalte Team könnte der Pandas Datentyp category zugewiesen werden, notwendig ist es aber nicht.

String ’ cm’ entfernen.

hooke.replace(' cm', '', regex = True, inplace = True)

Ob alle Elemente einer Zelle numerisch sind, kann mit der Pandas-Methode pd.Series.str.isnumeric() überprüft werden. Ein Blick auf die Daten zeigt die Ursache.

print(hooke['Gewicht'].str.isnumeric().sum())

print(hooke['Gewicht'].head())
print(hooke['Gewicht'].tail())
1
0    0
1    0
2    0
3    0
4    0
Name: Gewicht, dtype: object
532    850\n
533    850\n
534    850\n
535    850\n
536    850\n
Name: Gewicht, dtype: object

Die Zeilenumbrüche werden ebenfalls entfernt.

hooke.replace('\n', '', regex = True, inplace = True)
print(hooke['Gewicht'].str.isnumeric().sum())
print(hooke['Gewicht'].tail())
178
532    850
533    850
534    850
535    850
536    850
Name: Gewicht, dtype: object
print(hooke.info(), "\n")

# explizite Zuweisung
hooke['Abstand'] = hooke['Abstand'].astype('float')
hooke['Gewicht'] = hooke['Gewicht'].astype('float')

print(hooke.info())
<class 'pandas.core.frame.DataFrame'>
Index: 533 entries, 0 to 536
Data columns (total 4 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   Zeit     533 non-null    object
 1   Abstand  533 non-null    object
 2   Gewicht  533 non-null    object
 3   Team     533 non-null    object
dtypes: object(4)
memory usage: 20.8+ KB
None 

<class 'pandas.core.frame.DataFrame'>
Index: 533 entries, 0 to 536
Data columns (total 4 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   Zeit     533 non-null    object 
 1   Abstand  533 non-null    float64
 2   Gewicht  533 non-null    float64
 3   Team     533 non-null    object 
dtypes: float64(2), object(2)
memory usage: 20.8+ KB
None

Das Ergebnis könnte so aussehen:

hooke.groupby(by = ['Team', 'Gewicht'], sort = False)['Abstand'].describe()
count mean std min 25% 50% 75% max
Team Gewicht
fabi 0.0 10.0 110.366000 0.253693 109.95 110.2900 110.310 110.5800 110.74
50.0 12.0 110.425833 0.210992 110.22 110.3050 110.310 110.6225 110.77
101.0 9.0 108.238889 0.231163 107.96 107.9900 108.410 108.4200 108.47
152.0 10.0 106.902000 0.335685 106.45 106.6000 106.980 106.9950 107.43
201.0 10.0 105.253000 1.052416 102.30 105.4600 105.470 105.5775 105.90
253.0 11.0 104.279091 0.466036 103.55 103.9400 104.100 104.6600 104.91
302.0 12.0 102.650000 0.390943 102.09 102.4650 102.560 103.0100 103.35
353.0 11.0 100.673636 0.199613 100.50 100.5500 100.580 100.7800 101.03
403.0 14.0 99.447143 0.424815 98.97 99.1025 99.410 99.5900 100.26
455.0 11.0 97.839091 0.228099 97.51 97.6200 97.880 98.0350 98.15
die_ahnungslosen 0.0 10.0 109.759000 0.180705 109.55 109.6300 109.730 109.7925 110.09
50.0 8.0 108.912500 0.180930 108.53 108.8800 108.930 109.0250 109.11
100.0 12.0 107.725833 0.704446 106.76 107.1975 107.735 108.0725 108.92
150.0 19.0 106.265789 0.759505 104.67 105.7050 106.120 106.7100 107.87
200.0 18.0 104.616111 0.392955 103.96 104.2650 104.590 104.8150 105.23
250.0 18.0 102.906667 0.221519 102.35 102.8025 102.890 102.9750 103.33
300.0 11.0 101.840000 0.894561 101.00 101.3650 101.460 101.8150 104.15
350.0 14.0 100.264286 0.265930 99.98 100.1475 100.170 100.2625 101.06
400.0 11.0 99.239091 0.232786 98.94 99.0500 99.130 99.4150 99.57
450.0 12.0 98.042500 0.468016 97.51 97.6750 97.970 98.2850 99.08
kreativkoepfe 0.0 9.0 109.676667 0.053619 109.62 109.6400 109.640 109.7300 109.74
50.0 8.0 108.557500 0.489949 107.84 108.2325 108.495 108.8675 109.33
100.0 10.0 107.093000 0.922726 106.30 106.3800 106.875 107.2700 109.11
150.0 10.0 105.934000 0.599967 104.87 105.6125 106.020 106.3075 106.84
200.0 10.0 105.453000 1.618278 103.52 104.0750 105.035 106.7700 108.27
250.0 10.0 102.527000 0.796228 101.10 102.1700 102.410 102.7625 103.89
300.0 10.0 101.236000 0.702095 100.29 100.7625 101.010 101.8350 102.45
350.0 10.0 98.804000 1.471629 97.43 97.5725 98.155 100.3600 100.84
400.0 9.0 96.827778 1.036783 95.80 95.8900 96.900 97.4100 98.78
450.0 9.0 95.515556 1.115954 93.91 94.6300 95.750 95.8300 97.24
500.0 8.0 95.336250 1.564298 93.30 94.4575 95.140 96.5625 97.69
550.0 9.0 93.155556 0.352956 92.76 92.8700 92.950 93.4000 93.76
ma 0.0 10.0 109.258000 0.042374 109.18 109.2600 109.260 109.2800 109.31
50.0 10.0 107.179000 1.809588 105.83 106.0875 106.185 107.9575 110.81
100.0 10.0 102.101000 2.958804 99.37 99.6725 101.120 104.4825 106.67
150.0 11.0 106.656364 4.727985 100.29 100.9900 109.730 110.1600 110.60
200.0 9.0 108.456667 1.666816 106.74 107.2900 107.740 110.1700 111.06
250.0 10.0 106.931000 0.957838 105.66 105.9325 107.540 107.6975 107.80
300.0 10.0 105.696000 0.194376 105.46 105.5075 105.715 105.8650 105.94
350.0 10.0 104.838000 0.530949 104.10 104.5150 104.685 105.1475 105.94
400.0 10.0 104.790000 0.602974 104.31 104.4775 104.550 104.6925 106.09
450.0 10.0 104.517000 0.475513 103.91 104.2225 104.510 104.7125 105.56
500.0 10.0 104.456000 0.569097 103.65 104.0925 104.420 104.6400 105.71
550.0 10.0 103.004000 0.800475 101.55 102.7650 103.065 103.5000 104.20
600.0 10.0 101.403000 0.621183 100.22 101.5000 101.580 101.7800 101.94
650.0 10.0 100.109000 0.082523 99.97 100.0475 100.110 100.1875 100.21
700.0 10.0 98.871000 0.534155 98.24 98.5425 98.715 99.1850 100.02
750.0 9.0 97.966667 0.729315 96.86 97.5000 97.980 98.6800 98.77
800.0 11.0 97.115455 0.540210 96.19 96.7000 97.330 97.5050 97.72
850.0 8.0 95.715000 0.245822 95.35 95.5625 95.750 95.8775 96.07

5.3 Musterlösung Daten aufbereiten

Im nächsten Schritt werden die Daten geprüft und ggf. bereinigt. Dies umfasst die Schritte:

  • auf Ausreißer prüfen (studentisierte z-Werte und grafisch) und ggf. bereinigen,
  • Normierung der Abstandsmessung auf den Nullpunkt und Umrechnung der verwendeten Einheiten in SI-Einheiten und
  • grafische Darstellung.
Hinweis 5.1: glob, pd.groupby und SciPy

Die Ausführung des folgenden Codes kann lokal unterschiedlich ausfallen, je nach dem, in welcher Reihenfolge die Dateien mit dem Modul glob eingelesen wurden. Im Folgenden werden aus einem groupby-Objekt Indexpositionen bestimmt, an denen eine Bedingung wahr ist. Mit diesen Indexpositionen wird dann auf den ursprünglichen DataFrame zugegriffen. Pandas sortiert die Gruppen im groupby-Objekt standardmäßig alphabetisch (sort = True). Wenn die mit glob eingelesenen Dateien nicht in alphabetischer Reihenfolge vorliegen, wird mit falschen Indexpositionen auf den DataFrame zugegriffen.

Dem kann durch Sortieren des DataFrames nach den groupby-Kriterien begegnet werden oder, wie im Folgenden, durch das Argument pd.groupby(by = Bedingung, sort = False). Letztere Variante ist flexibler.

Auch gibt scipy.stats.zscore() je nach Version eine pd.Series oder ein np.array zurück. Dementsprechend sind die Methoden aus Pandas oder NumPy zu verwenden.

  1. Liegen ungültige Messungen vor?

Auf Ausreißer prüfen

Auf Ausreißer kann (unter anderem) mit studentisierten z-Werten und grafisch geprüft werden.

  1. Anzahl der Ausreißer bestimmen und Position ausgeben.
# z-Werte größer gleich abs(3) finden
## Lösung für SciPy 1.14.1 (gibt pd.Series zurück)
## z_values_ge3_sum = hooke.groupby(by = ['Team', 'Gewicht'])['Abstand'].apply(lambda x: scipy.stats.zscore(x, ddof = 1)).abs().ge(3).sum()

## Lösung für SciPy 1.15 und neuer (gibt np.array zurück)
z_values_ge3_sum = hooke.groupby(by = ['Team', 'Gewicht'], sort = False)['Abstand'].apply(scipy.stats.zscore, ddof = 1).apply(np.abs).apply(lambda x: np.greater_equal(x, 3)).apply(sum).sum()

print("Anzahl der studentisierten z-Werte mit Betrag ≥ 3:", z_values_ge3_sum, "\n")

# z-Werte größer gleich abs(2.5) finden
## Lösung für SciPy 1.14.1 (gibt pd.Series zurück)
## z_values_ge25_sum = hooke.groupby(by = ['Team', 'Gewicht'])['Abstand'].apply(lambda x: scipy.stats.zscore(x, ddof = 1)).abs().ge(2.5).sum()

## Lösung für SciPy 1.15 und neuer (gibt np.array zurück)
z_values_ge25_sum = hooke.groupby(by = ['Team', 'Gewicht'], sort = False)['Abstand'].apply(scipy.stats.zscore, ddof = 1).apply(np.abs).apply(lambda x: np.greater_equal(x, 2.5)).apply(sum).sum()
print("Anzahl der studentisierten z-Werte mit Betrag ≥ 2.5:", z_values_ge25_sum)

# Die Zeilen mit z-Werten größer abs(2.5) ausgeben
## Lösung für SciPy 1.14.1 (gibt pd.Series zurück)
## bool_index = hooke.groupby(by = ['Team', 'Gewicht'])['Abstand'].apply(lambda x: scipy.stats.zscore(x, ddof = 1)).abs().ge(2.5).values

## Lösung für SciPy 1.15 und neuer (gibt np.array zurück)
### Schritt 1: Wahrheitswerte abgreifen
bool_index = hooke.groupby(by = ['Team', 'Gewicht'], sort = False)['Abstand'].apply(scipy.stats.zscore, ddof = 1).apply(np.abs).apply(lambda x: np.greater_equal(x, 2.5)).values
### Schritt 2: Rückgabe in ein eindimensionales Array überführen: 
bool_index = np.hstack(bool_index)

print("\nAn diesen Indexpositionen liegen die Ausreißer:", np.nonzero(bool_index))
Anzahl der studentisierten z-Werte mit Betrag ≥ 3: 0 

Anzahl der studentisierten z-Werte mit Betrag ≥ 2.5: 4

An diesen Indexpositionen liegen die Ausreißer: (array([ 44, 186, 201, 206]),)
  1. Ausgabe der Zeilen aus den Messreihen
# Auswahl der Zeilen mit den z-Werten größer gleich 2.5
z_values_ge_25 = hooke.iloc[bool_index , :] # --> Hier muss das Problem liegen
print(z_values_ge_25, "\n")

# Kombinationen aus Gewicht & Team bestimmen
print("Kombinationen aus Gewicht & Team bestimmen")
teams = z_values_ge_25['Team'].unique()

## teams durchlaufen und jeweils die Gewichte speichern
team_gewichte = [] # leere liste
for i in range(len(teams)):
  print(teams[i])
  print(z_values_ge_25.loc[z_values_ge_25['Team'] == teams[i], 'Gewicht'], "\n")
  team_gewichte.append(z_values_ge_25.loc[z_values_ge_25['Team'] == teams[i], 'Gewicht'].values)

# print("Als Liste von arrays:")
# print(team_gewichte, "\n")
         Zeit  Abstand  Gewicht              Team
44   09:27:18   102.30    201.0              fabi
186  11:10:38   102.35    250.0  die_ahnungslosen
201  11:11:42   104.15    300.0  die_ahnungslosen
206  11:12:43   101.06    350.0  die_ahnungslosen 

Kombinationen aus Gewicht & Team bestimmen
fabi
44    201.0
Name: Gewicht, dtype: float64 

die_ahnungslosen
186    250.0
201    300.0
206    350.0
Name: Gewicht, dtype: float64 

Diese Ausgabe dient der Veranschaulichung und Kontrolle.

# Messreihen auswählen
messreihen = pd.DataFrame()
for i in range(len(teams)):
  for j in range(len(team_gewichte[i])):

    print(teams[i], team_gewichte[i][j])

    messreihen = pd.concat([messreihen, hooke.loc[ (hooke['Team'] == teams[i]) & (hooke['Gewicht'] == team_gewichte[i][j]) ]])

# studentisierte z-Werte der Messreihen bilden
## Lösung für SciPy 1.14.1 (gibt pd.Series zurück)
## messreihen_z_scores = messreihen.groupby(by = ['Team', 'Gewicht'])['Abstand'].apply(lambda x: scipy.stats.zscore(x, ddof = 1)).reset_index(drop = True)

## Lösung für SciPy 1.15 und neuer (gibt np.array zurück)
## Rückgabe zeilenweise in ein eindimensionales Array überführen: 
messreihen_z_scores = np.hstack(messreihen.groupby(by = ['Team', 'Gewicht'], sort = False)['Abstand'].apply(lambda x: scipy.stats.zscore(x, ddof = 1)).reset_index(drop = True))

# gemeinsame Ausgabe der Daten
## Lösung für SciPy 1.14.1 (gibt pd.Series zurück)
## messreihen.insert(loc = 2, column = 'z-Werte Abstand', value = messreihen_z_scores.values)

## Lösung für SciPy 1.15 und neuer (gibt np.array zurück)
messreihen.insert(loc = 2, column = 'z-Werte Abstand', value = messreihen_z_scores)
print(messreihen)
fabi 201.0
die_ahnungslosen 250.0
die_ahnungslosen 300.0
die_ahnungslosen 350.0
         Zeit  Abstand  z-Werte Abstand  Gewicht              Team
41   09:27:09   105.47         0.206192    201.0              fabi
42   09:27:12   105.90         0.614776    201.0              fabi
43   09:27:15   105.54         0.272706    201.0              fabi
44   09:27:18   102.30        -2.805925    201.0              fabi
45   09:27:21   105.90         0.614776    201.0              fabi
46   09:27:24   105.47         0.206192    201.0              fabi
47   09:27:27   105.44         0.177686    201.0              fabi
48   09:27:30   105.59         0.320216    201.0              fabi
49   09:27:33   105.46         0.196690    201.0              fabi
50   09:27:36   105.46         0.196690    201.0              fabi
177  11:10:11   102.99         0.376191    250.0  die_ahnungslosen
178  11:10:13   102.99         0.376191    250.0  die_ahnungslosen
179  11:10:17   102.93         0.105333    250.0  die_ahnungslosen
180  11:10:20   102.83        -0.346095    250.0  die_ahnungslosen
181  11:10:23   103.16         1.143620    250.0  die_ahnungslosen
182  11:10:26   102.80        -0.481524    250.0  die_ahnungslosen
183  11:10:29   102.90        -0.030095    250.0  die_ahnungslosen
184  11:10:32   103.33         1.911049    250.0  die_ahnungslosen
185  11:10:35   102.78        -0.571810    250.0  die_ahnungslosen
186  11:10:38   102.35        -2.512954    250.0  die_ahnungslosen
187  11:10:41   102.93         0.105333    250.0  die_ahnungslosen
188  11:10:44   102.88        -0.120381    250.0  die_ahnungslosen
189  11:10:47   102.90        -0.030095    250.0  die_ahnungslosen
190  11:10:50   102.88        -0.120381    250.0  die_ahnungslosen
191  11:10:53   102.73        -0.797524    250.0  die_ahnungslosen
192  11:10:56   102.81        -0.436381    250.0  die_ahnungslosen
193  11:10:59   102.80        -0.481524    250.0  die_ahnungslosen
194  11:11:02   103.33         1.911049    250.0  die_ahnungslosen
195  11:11:24   101.37        -0.525397    300.0  die_ahnungslosen
196  11:11:27   101.43        -0.458325    300.0  die_ahnungslosen
197  11:11:30   102.80         1.073152    300.0  die_ahnungslosen
198  11:11:33   101.72        -0.134144    300.0  die_ahnungslosen
199  11:11:36   101.36        -0.536576    300.0  die_ahnungslosen
200  11:11:39   101.46        -0.424789    300.0  die_ahnungslosen
201  11:11:42   104.15         2.582271    300.0  die_ahnungslosen
202  11:11:45   101.36        -0.536576    300.0  die_ahnungslosen
203  11:11:48   101.00        -0.939008    300.0  die_ahnungslosen
204  11:11:51   101.91         0.078251    300.0  die_ahnungslosen
205  11:11:54   101.68        -0.178859    300.0  die_ahnungslosen
206  11:12:43   101.06         2.992196    350.0  die_ahnungslosen
208  11:12:46   100.17        -0.354551    350.0  die_ahnungslosen
209  11:12:49    99.98        -1.069025    350.0  die_ahnungslosen
210  11:12:52   100.07        -0.730590    350.0  die_ahnungslosen
211  11:12:55   100.21        -0.204135    350.0  die_ahnungslosen
212  11:12:58   100.21        -0.204135    350.0  die_ahnungslosen
213  11:13:01   100.14        -0.467363    350.0  die_ahnungslosen
214  11:13:04   100.17        -0.354551    350.0  die_ahnungslosen
215  11:13:07   100.28         0.059092    350.0  die_ahnungslosen
216  11:13:10   100.55         1.074397    350.0  die_ahnungslosen
217  11:13:13   100.14        -0.467363    350.0  die_ahnungslosen
218  11:13:16   100.17        -0.354551    350.0  die_ahnungslosen
219  11:13:19   100.17        -0.354551    350.0  die_ahnungslosen
220  11:13:22   100.38         0.435131    350.0  die_ahnungslosen

Die Werte können mit der Pandas-Methode pd.plot() mit wenig Aufwand dargestellt werden. Die Methode ist jedoch nicht so flexibel, wie das Paket matplotlib. So ist das Punktdiagramm (kind = 'scatter') nur für DataFrames, nicht aber für groupby-Objekte verfügbar. Dies wird durch das Setzen eines Markers und die Einstellung der Liniendicke auf 0 kompensiert.

messreihen.reset_index(drop = True).groupby(by = ['Team', 'Gewicht'], sort = False)['Abstand'].plot(marker = 'o', lw = 0)

plt.xlabel('Messwert')
plt.ylabel('Abstand')
plt.legend()

plt.show()

Grafische Darstellung der Messreihen, die studentisierte z-Werte >= 2,5 enthalten.

 

Die Werte, die betragsmäßig studentisierte z-Werte \(\ge\) 2,5 aufweisen, könnten als Ausreißer entfernt werden. In diesem Fall wird darauf verzichtet.

Umwandlung der Rechengrößen

Im nächsten Schritt wird die Abstandsmessung auf den Nullpunkt normiert, um die Federausdehnung abzubilden. Ebenso wird das Gewicht in \(g\) in die wirkende Kraft in \(N\) umgerechnet.

Abstandsmessung auf Meter und auf den Nullpunkt normieren

Abstandsmessung auf den Nullpunkt normieren. Die Spalte Abstand wird in Abständsänderung umbenannt.

nullpunkte = hooke.loc[hooke['Gewicht'] == 0, : ].groupby(by = 'Team', sort = False)['Abstand'].mean()

print("Nullpunkte", nullpunkte, sep = '\n')

teams = nullpunkte.index

for i in range(len(teams)):

  hooke.loc[hooke['Team'] == teams[i] , 'Abstand'] = hooke.loc[hooke['Team'] == teams[i] , 'Abstand'].sub(nullpunkte.values[i]).mul(-1)

hooke.rename(columns = {'Abstand': 'Abstandsänderung'}, inplace = True)
hooke['Abstandsänderung'] = hooke['Abstandsänderung'].div(100)
Nullpunkte
Team
fabi                110.366000
die_ahnungslosen    109.759000
kreativkoepfe       109.676667
ma                  109.258000
Name: Abstand, dtype: float64

Gewicht in wirkende Kraft umrechnen

Gewicht in \(g\) in die wirkende Kraft in \(N\) umrechnen. Die Spalte wird in den Datensatz eingefügt.

hooke['Kraft'] = hooke['Gewicht'].div(1000).mul(9.81)

Das Ergebnis könnte so aussehen. Die Spalte Abstand wurde in Abständsänderung umbenannt.

hooke.groupby(by = ['Team', 'Kraft'], sort = False)['Abstandsänderung'].describe()
count mean std min 25% 50% 75% max
Team Kraft
fabi 0.00000 10.0 1.137111e-16 0.002537 -0.003740 -0.002140 0.000560 0.000760 0.004160
0.49050 12.0 -5.983333e-04 0.002110 -0.004040 -0.002565 0.000560 0.000610 0.001460
0.99081 9.0 2.127111e-02 0.002312 0.018960 0.019460 0.019560 0.023760 0.024060
1.49112 10.0 3.464000e-02 0.003357 0.029360 0.033710 0.033860 0.037660 0.039160
1.97181 10.0 5.113000e-02 0.010524 0.044660 0.047885 0.048960 0.049060 0.080660
2.48193 11.0 6.086909e-02 0.004660 0.054560 0.057060 0.062660 0.064260 0.068160
2.96262 12.0 7.716000e-02 0.003909 0.070160 0.073560 0.078060 0.079010 0.082760
3.46293 11.0 9.692364e-02 0.001996 0.093360 0.095860 0.097860 0.098160 0.098660
3.95343 14.0 1.091886e-01 0.004248 0.101060 0.107760 0.109560 0.112635 0.113960
4.46355 11.0 1.252691e-01 0.002281 0.122160 0.123310 0.124860 0.127460 0.128560
die_ahnungslosen 0.00000 10.0 -1.420088e-16 0.001807 -0.003310 -0.000335 0.000290 0.001290 0.002090
0.49050 8.0 8.465000e-03 0.001809 0.006490 0.007340 0.008290 0.008790 0.012290
0.98100 12.0 2.033167e-02 0.007044 0.008390 0.016865 0.020240 0.025615 0.029990
1.47150 19.0 3.493211e-02 0.007595 0.018890 0.030490 0.036390 0.040540 0.050890
1.96200 18.0 5.142889e-02 0.003930 0.045290 0.049440 0.051690 0.054940 0.057990
2.45250 18.0 6.852333e-02 0.002215 0.064290 0.067840 0.068690 0.069565 0.074090
2.94300 11.0 7.919000e-02 0.008946 0.056090 0.079440 0.082990 0.083940 0.087590
3.43350 14.0 9.494714e-02 0.002659 0.086990 0.094965 0.095890 0.096115 0.097790
3.92400 11.0 1.051991e-01 0.002328 0.101890 0.103440 0.106290 0.107090 0.108190
4.41450 12.0 1.171650e-01 0.004680 0.106790 0.114740 0.117890 0.120840 0.122490
kreativkoepfe 0.00000 9.0 7.894196e-17 0.000536 -0.000633 -0.000533 0.000367 0.000367 0.000567
0.49050 8.0 1.119167e-02 0.004899 0.003467 0.008092 0.011817 0.014442 0.018367
0.98100 10.0 2.583667e-02 0.009227 0.005667 0.024067 0.028017 0.032967 0.033767
1.47150 10.0 3.742667e-02 0.006000 0.028367 0.033692 0.036567 0.040642 0.048067
1.96200 10.0 4.223667e-02 0.016183 0.014067 0.029067 0.046417 0.056017 0.061567
2.45250 10.0 7.149667e-02 0.007962 0.057867 0.069142 0.072667 0.075067 0.085767
2.94300 10.0 8.440667e-02 0.007021 0.072267 0.078417 0.086667 0.089142 0.093867
3.43350 10.0 1.087267e-01 0.014716 0.088367 0.093167 0.115217 0.121042 0.122467
3.92400 9.0 1.284889e-01 0.010368 0.108967 0.122667 0.127767 0.137867 0.138767
4.41450 9.0 1.416111e-01 0.011160 0.124367 0.138467 0.139267 0.150467 0.157667
4.90500 8.0 1.434042e-01 0.015643 0.119867 0.131142 0.145367 0.152192 0.163767
5.39550 9.0 1.652111e-01 0.003530 0.159167 0.162767 0.167267 0.168067 0.169167
ma 0.00000 10.0 -7.105997e-17 0.000424 -0.000520 -0.000220 -0.000020 -0.000020 0.000780
0.49050 10.0 2.079000e-02 0.018096 -0.015520 0.013005 0.030730 0.031705 0.034280
0.98100 10.0 7.157000e-02 0.029588 0.025880 0.047755 0.081380 0.095855 0.098880
1.47150 11.0 2.601636e-02 0.047280 -0.013420 -0.009020 -0.004720 0.082680 0.089680
1.96200 9.0 8.013333e-03 0.016668 -0.018020 -0.009120 0.015180 0.019680 0.025180
2.45250 10.0 2.327000e-02 0.009578 0.014580 0.015605 0.017180 0.033255 0.035980
2.94300 10.0 3.562000e-02 0.001944 0.033180 0.033930 0.035430 0.037505 0.037980
3.43350 10.0 4.420000e-02 0.005309 0.033180 0.041105 0.045730 0.047430 0.051580
3.92400 10.0 4.468000e-02 0.006030 0.031680 0.045655 0.047080 0.047805 0.049480
4.41450 10.0 4.741000e-02 0.004755 0.036980 0.045455 0.047480 0.050355 0.053480
4.90500 10.0 4.802000e-02 0.005691 0.035480 0.046180 0.048380 0.051655 0.056080
5.39550 10.0 6.254000e-02 0.008005 0.050580 0.057580 0.061930 0.064930 0.077080
5.88600 10.0 7.855000e-02 0.006212 0.073180 0.074780 0.076780 0.077580 0.090380
6.37650 10.0 9.149000e-02 0.000825 0.090480 0.090705 0.091480 0.092105 0.092880
6.86700 10.0 1.038700e-01 0.005342 0.092380 0.100730 0.105430 0.107155 0.110180
7.35750 9.0 1.129133e-01 0.007293 0.104880 0.105780 0.112780 0.117580 0.123980
7.84800 11.0 1.214255e-01 0.005402 0.115380 0.117530 0.119280 0.125580 0.130680
8.33850 8.0 1.354300e-01 0.002458 0.131880 0.133805 0.135080 0.136955 0.139080

Grafische Darstellung

Da es nur vier Teams gibt, können die Messreihen grafisch dargestellt werden. Eine mögliche Darstellung können Sie dem ersten Reiter, die Zwischenschritte und Schlussfolgerungen den folgenden Reitern entnehmen.

Auf der x-Achse ist die Federausdehnung in m, auf der y-Achse die wirkende Kraft in N abgetragen. Zusätzlich zu den Messpunkten sind für jedes Gewicht der Mittelwert mit einem großen X und von dessen Mittelpunkt ausgehend der Bereich des Mittelwerts ± 1 Standardfehler eingezeichnet.

Die Ausgabe ist aus Platzgründen auf die ersten Zeilen beschränkt.

Mittelwerte der Abstandsänderung nach Team und Kraft:
Team  Kraft  
fabi  0.00000    1.137111e-16
      0.49050   -5.983333e-04
      0.99081    2.127111e-02
      1.49112    3.464000e-02
      1.97181    5.113000e-02
Name: Federausdehnung, dtype: float64

Standardfehler der Mittelwerte nach Team und Kraft
Team  Kraft  
fabi  0.00000    0.000802
      0.49050    0.000609
      0.99081    0.000771
      1.49112    0.001062
      1.97181    0.003328
Name: Standardfehler, dtype: float64
# Mittelwerte der Teams nach Kraft
distance_means_by_team_and_force = hooke.groupby(by = [hooke['Team'], hooke['Kraft']], sort = False)['Abstandsänderung'].mean()
distance_means_by_team_and_force.name = 'Federausdehnung'

print("Mittelwerte der Abstandsänderung nach Team und Kraft:", distance_means_by_team_and_force.head(), sep = '\n')

print() # leere Zeile

# Standardfehler der Teams nach Kraft
distance_stderrors_by_team_and_force = hooke.groupby(by = [hooke['Team'], hooke['Kraft']], sort = False)['Abstandsänderung'].std(ddof = 1).div(np.sqrt(hooke['Abstandsänderung'].groupby(by = [hooke['Team'], hooke['Kraft']], sort = False).size()))
distance_stderrors_by_team_and_force.name = 'Standardfehler'

print("Standardfehler der Mittelwerte nach Team und Kraft", distance_stderrors_by_team_and_force.head(), sep = '\n')

# grafische Darstellung
anzahl_teams = hooke['Team'].unique().size

plt.figure(figsize = (7.5, 12))
for i in range(anzahl_teams):
  
  plt.subplot(4, 1, i + 1) # plt.subplot zählt ab 1

  # Punktdiagramm
  plotting_data = hooke.loc[hooke['Team'] == hooke['Team'].unique()[i], :]
  plt.scatter(x = plotting_data['Abstandsänderung'], y = plotting_data['Kraft'], alpha = 0.6)

  plt.title(label = hooke['Team'].unique()[i])
  plt.xlabel("Federausdehnung in m")
  plt.ylabel("wirkende Kraft in N")

  # # Fehlerbalken
  distance_means_by_force = plotting_data.groupby(by = plotting_data['Kraft'], sort = False)['Abstandsänderung'].mean()
  distance_stderrors_by_force = plotting_data.groupby(by = plotting_data['Kraft'], sort = False)['Abstandsänderung'].std(ddof = 1).div(np.sqrt(plotting_data['Abstandsänderung'].groupby(by = plotting_data['Kraft'], sort = False).size()))

  errorbar_container = plt.errorbar(x = distance_means_by_force, y = distance_means_by_force.index, xerr = distance_stderrors_by_force,
  linestyle = 'none', marker = 'x', color = 'black', markersize = 12, elinewidth = 3, ecolor = 'red', capsize = 12)

  # siehe: https://matplotlib.org/stable/api/container_api.html#matplotlib.container.ErrorbarContainer
  plt.legend([errorbar_container.lines[0], errorbar_container.lines[2][0]],
             ['Mittelwert', 'Standardfehler'],
             loc = 'upper left')

plt.tight_layout()
plt.show()
  • Die Messreihen des Teams die_ahnungslosen entsprechen dem erwarteten linearen Trend.
  • Bei Team fabi scheint für das erste angehängte Gewicht (50 Gramm) ein Fehler bei der Datenerhebung vorzuliegen. Vermutlich wurde hier mit 0 Gramm gemessen.
  • Die Messreihen des Teams kreativköpfe entsprechen weitgehend dem erwarteten linearen Trend.
  • Die Messreihen des Teams ma scheinen wenigstens für die ersten vier angehängten Gewichten durch grobe Messfehler geprägt zu sein.

Die Messreihe des Teams ma wird wegen grober Messfehler aus dem Datensatz entfernt. Aus der Messreihe des Teams fabi wird die Messung für das Gewicht 50 Gramm entfernt.

hooke.drop(index = hooke.loc[hooke['Team'] == 'ma', :].index, inplace = True)
hooke.drop(index = hooke.loc[(hooke['Team'] == 'fabi') & (hooke['Gewicht'] == 50), :].index, inplace = True)

 

Tipp 5.4: Vorgehen bei vielen Datensätzen

Bei einer großen Anzahl an Datensätzen kann auch die grafische Kontrolle an Grenzen stoßen. In diesem Fall empfiehlt es sich, die visuellen und kennzahlenbasierten Methoden zusammen zu nutzen, um Muster zu identifizieren und für eine große Zahl von Messungen zu überprüfen. Beispielsweise könnten nach einer visuellen Inspektion von Messreihen mit Extremwerten bzw. Ausreißern alle Messreihen daraufhin überprüft werden, ob mit zunehmenden Gewicht stets auch die mittlere Federausdehnung größer als für leichtere Gewichte ist. Abweichende Messreihen könnten dann grafisch kontrolliert werden.

5.4 Musterlösung Federkonstanten bestimmen

Im nächsten Schritt können die Federkonstanten mittels linearer Regression bestimmt werden.

  1. Welche Werte können für die Federkonstanten ermittelt werden?
  2. Wurden die Messungen mit der gleichen Feder durchgeführt, wenn als Vertrauenswahrscheinlichkeit 90 % bzw. 95 % angenommen werden soll?

fabi Konfidenzniveau: 0.9
y = 0.2373 + 34.1048 * x
r = 0.9907 R2 = 0.9814 p = 0.0000
Standardfehler des Anstiegs: 0.4792
33.309  ≤ 34.105 ≤ 34.901

die_ahnungslosen Konfidenzniveau: 0.9
y = 0.1798 + 34.9183 * x
r = 0.9886 R2 = 0.9773 p = 0.0000
Standardfehler des Anstiegs: 0.4651
34.148  ≤ 34.918 ≤ 35.689

kreativkoepfe Konfidenzniveau: 0.9
y = 0.3180 + 29.7643 * x
r = 0.9773 R2 = 0.9552 p = 0.0000
Standardfehler des Anstiegs: 0.6148
28.744  ≤ 29.764 ≤ 30.784

fabi Konfidenzniveau: 0.95
y = 0.2373 + 34.1048 * x
r = 0.9907 R2 = 0.9814 p = 0.0000
Standardfehler des Anstiegs: 0.4792
33.154  ≤ 34.105 ≤ 35.056

die_ahnungslosen Konfidenzniveau: 0.95
y = 0.1798 + 34.9183 * x
r = 0.9886 R2 = 0.9773 p = 0.0000
Standardfehler des Anstiegs: 0.4651
33.998  ≤ 34.918 ≤ 35.838

kreativkoepfe Konfidenzniveau: 0.95
y = 0.3180 + 29.7643 * x
r = 0.9773 R2 = 0.9552 p = 0.0000
Standardfehler des Anstiegs: 0.6148
28.546  ≤ 29.764 ≤ 30.983
anzahl_teams = hooke['Team'].unique().size
alpha = 0.10

for i in range(anzahl_teams):

  reg_data = hooke.loc[hooke['Team'] == hooke['Team'].unique()[i], :]
  x = reg_data['Abstandsänderung']
  y = reg_data['Kraft']
  n = len(x)
  
  print("\n", hooke['Team'].unique()[i], " Konfidenzniveau: ", 1 - alpha, sep = '')

  slope, intercept, rvalue, pvalue, slope_stderr = scipy.stats.linregress(x, y)
  print(f"y = {intercept:.4f} + {slope:.4f} * x\n",
        f"r = {rvalue:.4f} R2 = {rvalue ** 2:.4f} p = {pvalue:.4f}\n",
        f"Standardfehler des Anstiegs: {slope_stderr:.4f}", sep = '')

  print(f"{slope - scipy.stats.t.ppf(q = 1 - alpha / 2, df = n - 2) * slope_stderr:.3f}{slope:.3f}{slope + scipy.stats.t.ppf(q = 1 - alpha / 2, df = n - 2) * slope_stderr:.3f}")

alpha = 0.05

for i in range(anzahl_teams):

  reg_data = hooke.loc[hooke['Team'] == hooke['Team'].unique()[i], :]
  x = reg_data['Abstandsänderung']
  y = reg_data['Kraft']
  n = len(x)
  
  print("\n", hooke['Team'].unique()[i], " Konfidenzniveau: ", 1 - alpha, sep = '')

  slope, intercept, rvalue, pvalue, slope_stderr = scipy.stats.linregress(x, y)
  print(f"y = {intercept:.4f} + {slope:.4f} * x\n",
        f"r = {rvalue:.4f} R2 = {rvalue ** 2:.4f} p = {pvalue:.4f}\n",
        f"Standardfehler des Anstiegs: {slope_stderr:.4f}", sep = '')

  print(f"{slope - scipy.stats.t.ppf(q = 1 - alpha / 2, df = n - 2) * slope_stderr:.3f}{slope:.3f}{slope + scipy.stats.t.ppf(q = 1 - alpha / 2, df = n - 2) * slope_stderr:.3f}")

Die Punktschätzung der Federkonstante von Team fabi 34.105 liegt im 95-%-Konfidenzintervall der Messung von Team die_ahnungslosen 33.998 ≤ 34.918 ≤ 35.838. Die Punktschätzung der Federkonstante von Team fabi 34.105 liegt aber nicht im 90-%-Konfidenzintervall der Messung von Team die_ahnungslosen 34.148 ≤ 34.918 ≤ 35.689.

Unabhängig vom gewählten Vertrauensniveau liegt die Punktschätzung der Federkonstante von Team kreativkoepfe 29.764 nicht in den Konfidenzintervallen der beiden übrigen Teams.

Hinweis 5.2: Ergebnisse

Abhängig vom gewählten Vorgehen sind andere Ergebnisse möglich, beispielsweise durch das Entfernen von als Ausreißern eingestuften Einzelwerten oder einer anderen Behandlung der Messreihe vom Team fabi für das angehängte Gewicht 50 Gramm.