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_ma.txt', 'team_fabi.txt', 'team_kreativkoepfe.txt', 'team_die_ahnungslosen.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_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_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_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

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

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, error, sep = "\n")
    pfad_problem_datei = pfad

print(hooke.info(), "\n")
print("Erfolgreich einglesen:\n", hooke['Team'].unique(), sep = '')
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' 'kreativkoepfe' 'die_ahnungslosen']

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.

Der Code muss ggf. noch angepasst werden, weil vermutlich so leere zeilen angefügt werden

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
    # Dateinamen anfügen
    zwischenspeicher.append(pfad[5:-4])

    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' 'kreativkoepfe' 'die_ahnungslosen']

<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
113  NaN     NaN     NaN     kreativkoepfe
133  NaN     NaN     NaN     kreativkoepfe
138  NaN     NaN     NaN     kreativkoepfe
322  NaN     NaN     NaN  die_ahnungslosen

… 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'])['Abstand'].describe()
count mean std min 25% 50% 75% max
Team Gewicht
die_ahnungslosen 0.0 20.0 109.508500 0.287004 109.18 109.2600 109.430 109.7300 110.09
50.0 18.0 107.949444 1.591453 105.83 106.1625 108.675 108.9925 110.81
100.0 22.0 105.169091 3.497103 99.37 101.4375 106.770 107.8275 108.92
150.0 30.0 106.409000 2.846562 100.29 105.6800 106.480 107.6875 110.60
200.0 27.0 105.896296 2.087973 103.96 104.5350 104.860 107.2550 111.06
250.0 28.0 104.343929 2.047615 102.35 102.8675 102.990 105.9325 107.80
300.0 21.0 103.676190 2.076371 101.00 101.4600 104.150 105.5800 105.94
350.0 24.0 102.170000 2.335764 99.98 100.1700 100.465 104.5350 105.94
400.0 21.0 101.882381 2.874138 98.94 99.1300 99.570 104.5500 106.09
450.0 22.0 100.985455 3.331626 97.51 97.9250 98.805 104.4175 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
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
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

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.
  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.

Anzahl der studentisierten z-Werte mit Betrag ≥ 3: 0 

Anzahl der studentisierten z-Werte mit Betrag ≥ 2.5: 1
         Zeit  Abstand  Gewicht              Team
359  10:09:38   109.26      0.0  die_ahnungslosen 

Kombinationen aus Gewicht & Team bestimmen
die_ahnungslosen
359    0.0
Name: Gewicht, dtype: float64 
die_ahnungslosen 0.0
         Zeit  Abstand  z-Werte Abstand  Gewicht              Team
225  11:03:23   109.66         0.527867      0.0  die_ahnungslosen
226  11:03:23   109.62         0.388496      0.0  die_ahnungslosen
227  11:03:23   109.73         0.771766      0.0  die_ahnungslosen
228  11:03:23   109.55         0.144597      0.0  die_ahnungslosen
229  11:03:23   110.09         2.026104      0.0  die_ahnungslosen
230  11:03:23   109.73         0.771766      0.0  die_ahnungslosen
231  11:03:23   110.05         1.886733      0.0  die_ahnungslosen
232  11:03:23   109.81         1.050508      0.0  die_ahnungslosen
233  11:03:23   109.61         0.353654      0.0  die_ahnungslosen
234  11:03:23   109.74         0.806609      0.0  die_ahnungslosen
359  10:09:38   109.26        -0.865841      0.0  die_ahnungslosen
360  10:09:41   109.26        -0.865841      0.0  die_ahnungslosen
361  10:09:44   109.28        -0.796156      0.0  die_ahnungslosen
362  10:09:47   109.18        -1.144583      0.0  die_ahnungslosen
363  10:09:50   109.28        -0.796156      0.0  die_ahnungslosen
364  10:09:53   109.19        -1.109740      0.0  die_ahnungslosen
365  10:09:56   109.31        -0.691628      0.0  die_ahnungslosen
366  10:09:59   109.30        -0.726471      0.0  die_ahnungslosen
367  10:10:02   109.26        -0.865841      0.0  die_ahnungslosen
368  10:10:05   109.26        -0.865841      0.0  die_ahnungslosen
# z-Werte größer gleich abs(3) finden
z_values_ge3_sum = hooke.groupby(by = ['Team', 'Gewicht'])['Abstand'].apply(lambda x: scipy.stats.zscore(x, ddof = 1)).abs().ge(3).sum()
print("Anzahl der studentisierten z-Werte mit Betrag ≥ 3:", z_values_ge3_sum)

# z-Werte größer gleich abs(2.5) finden
z_values_ge25_sum = hooke.groupby(by = ['Team', 'Gewicht'])['Abstand'].apply(lambda x: scipy.stats.zscore(x, ddof = 1)).abs().ge(2.5).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
bool_index = hooke.groupby(by = ['Team', 'Gewicht'])['Abstand'].apply(lambda x: scipy.stats.zscore(x, ddof = 1)).abs().ge(2.5).values
z_values_ge_25 = hooke.iloc[bool_index , :]
print(z_values_ge_25, "\n")

# 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(team_gewichte, "\n")

# 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
messreihen_z_scores = messreihen.groupby(by = ['Team', 'Gewicht'])['Abstand'].apply(lambda x: scipy.stats.zscore(x, ddof = 1)).reset_index(drop = True)

# gemeinsame Ausgabe der Daten
messreihen.insert(loc = 2, column = 'z-Werte Abstand', value = messreihen_z_scores.values)
print(messreihen)

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'])['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')['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
die_ahnungslosen    109.508500
fabi                110.366000
kreativkoepfe       109.676667
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'])['Abstandsänderung'].describe()
count mean std min 25% 50% 75% max
Team Kraft
die_ahnungslosen 0.00000 20.0 -3.560520e-17 0.002870 -0.005815 -0.002215 0.000785 0.002485 0.003285
0.49050 18.0 1.559056e-02 0.015915 -0.013015 0.005160 0.008335 0.033460 0.036785
0.98100 22.0 4.339409e-02 0.034971 0.005885 0.016810 0.027385 0.080710 0.101385
1.47150 30.0 3.099500e-02 0.028466 -0.010915 0.018210 0.030285 0.038285 0.092185
1.96200 27.0 3.612204e-02 0.020880 -0.015515 0.022535 0.046485 0.049735 0.055485
2.45250 28.0 5.164571e-02 0.020476 0.017085 0.035760 0.065185 0.066410 0.071585
2.94300 21.0 5.832310e-02 0.020764 0.035685 0.039285 0.053585 0.080485 0.085085
3.43350 24.0 7.338500e-02 0.023358 0.035685 0.049735 0.090435 0.093385 0.095285
3.92400 21.0 7.626119e-02 0.028741 0.034185 0.049585 0.099385 0.103785 0.105685
4.41450 22.0 8.523045e-02 0.033316 0.039485 0.050910 0.107035 0.115835 0.119985
4.90500 10.0 5.052500e-02 0.005691 0.037985 0.048685 0.050885 0.054160 0.058585
5.39550 10.0 6.504500e-02 0.008005 0.053085 0.060085 0.064435 0.067435 0.079585
5.88600 10.0 8.105500e-02 0.006212 0.075685 0.077285 0.079285 0.080085 0.092885
6.37650 10.0 9.399500e-02 0.000825 0.092985 0.093210 0.093985 0.094610 0.095385
6.86700 10.0 1.063750e-01 0.005342 0.094885 0.103235 0.107935 0.109660 0.112685
7.35750 9.0 1.154183e-01 0.007293 0.107385 0.108285 0.115285 0.120085 0.126485
7.84800 11.0 1.239305e-01 0.005402 0.117885 0.120035 0.121785 0.128085 0.133185
8.33850 8.0 1.379350e-01 0.002458 0.134385 0.136310 0.137585 0.139460 0.141585
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
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

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 
die_ahnungslosen  0.0000   -3.558352e-17
                  0.4905    1.559056e-02
                  0.9810    4.339409e-02
                  1.4715    3.099500e-02
                  1.9620    3.612204e-02
Name: Federausdehnung, dtype: float64

Standardfehler der Mittelwerte nach Team und Kraft
Team              Kraft 
die_ahnungslosen  0.0000    0.000642
                  0.4905    0.003751
                  0.9810    0.007456
                  1.4715    0.005197
                  1.9620    0.004018
Name: Standardfehler, dtype: float64
# Mittelwerte der Teams nach Kraft
distance_means_by_team_and_force = hooke.groupby(by = [hooke['Team'], hooke['Kraft']])['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']])['Abstandsänderung'].std(ddof = 1).div(np.sqrt(hooke['Abstandsänderung'].groupby(by = [hooke['Team'], hooke['Kraft']]).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'])['Abstandsänderung'].mean()
  distance_stderrors_by_force = plotting_data.groupby(by = plotting_data['Kraft'])['Abstandsänderung'].std(ddof = 1).div(np.sqrt(plotting_data['Abstandsänderung'].groupby(by = plotting_data['Kraft']).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

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

die_ahnungslosen Konfidenzniveau: 0.9
y = 0.5701 + 46.2192 * x
r = 0.7944 R2 = 0.6311 p = 0.0000
Standardfehler des Anstiegs: 2.0101
42.903  ≤ 46.219 ≤ 49.535

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

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

die_ahnungslosen Konfidenzniveau: 0.95
y = 0.5701 + 46.2192 * x
r = 0.7944 R2 = 0.6311 p = 0.0000
Standardfehler des Anstiegs: 2.0101
42.264  ≤ 46.219 ≤ 50.174
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.1: 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.