Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 450 – Hinzufügen eines Statistikmoduls zur Standardbibliothek

Autor:
Steven D’Aprano <steve at pearwood.info>
Status:
Final
Typ:
Standards Track
Erstellt:
01.08.2013
Python-Version:
3.4
Post-History:
13.09.2013

Inhaltsverzeichnis

Zusammenfassung

Dieses PEP schlägt die Aufnahme eines Moduls für gängige Statistikfunktionen wie Mittelwert, Median, Varianz und Standardabweichung in die Python-Standardbibliothek vor. Siehe auch http://bugs.python.org/issue18606

Begründung

Das vorgeschlagene Statistikmodul wird durch die „Batteries Included“-Philosophie der Python-Standardbibliothek motiviert. Raymond Hettinger und andere leitende Entwickler haben nach einer qualitativ hochwertigen Statistikbibliothek gefragt, die irgendwo zwischen High-End-Statistikbibliotheken und Ad-hoc-Code liegt. [1] Statistische Funktionen wie Mittelwert, Standardabweichung und andere sind offensichtliche und nützliche Bestandteile, die jedem Schüler bekannt sind. Selbst billige wissenschaftliche Taschenrechner enthalten typischerweise mehrere statistische Funktionen wie

  • Mittelwert
  • Populations- und Stichprobenvarianz
  • Populations- und Stichprobenstandardabweichung
  • Lineare Regression
  • Korrelationskoeffizient

Grafiktaschenrechner für Schüler enthalten typischerweise alle oben genannten sowie einige oder alle der folgenden Funktionen:

  • Median
  • Modus
  • Funktionen zur Berechnung der Wahrscheinlichkeit von Zufallsvariablen aus den Normal-, t-, Chi-Quadrat- und F-Verteilungen
  • Schlussfolgerungen über den Mittelwert

und andere [2]. Ebenso enthalten Tabellenkalkulationsprogramme wie Microsoft Excel, LibreOffice und Gnumeric umfangreiche Sammlungen von Statistikfunktionen [3].

Im Gegensatz dazu hat Python derzeit keinen Standardweg, um auch nur die einfachsten und offensichtlichsten statistischen Funktionen wie den Mittelwert zu berechnen. Für diejenigen, die Statistikfunktionen in Python benötigen, gibt es zwei offensichtliche Lösungen:

  • numpy und/oder scipy installieren [4];
  • oder eine Do-It-Yourself-Lösung verwenden.

Numpy ist vielleicht die funktionsreichste Lösung, aber sie hat einige Nachteile:

  • Sie ist für viele Zwecke übertrieben. Die Dokumentation von numpy warnt sogar:
    „Es kann schwierig sein zu wissen, welche Funktionen in numpy verfügbar sind. Dies ist keine vollständige Liste, deckt aber die meisten ab.“[5]

    und listet dann über 270 Funktionen auf, von denen nur wenige mit Statistik zu tun haben.

  • Numpy richtet sich an diejenigen, die intensive numerische Arbeiten durchführen, und kann für diejenigen einschüchternd sein, die keinen Hintergrund in computergestützter Mathematik und Informatik haben. Zum Beispiel nimmt numpy.mean vier Argumente entgegen
    mean(a, axis=None, dtype=None, out=None)
    

    obwohl, glücklicherweise für den Anfänger oder Gelegenheitsnutzer von numpy, drei optional sind und numpy.mean in einfachen Fällen das Richtige tut.

    >>>  numpy.mean([1, 2, 3, 4])
    2.5
    
  • Für viele Menschen kann die Installation von numpy schwierig oder unmöglich sein. Zum Beispiel müssen Personen in Unternehmensumgebungen einen schwierigen, zeitaufwendigen Prozess durchlaufen, bevor ihnen die Installation von Drittanbietersoftware gestattet wird. Für den gelegentlichen Python-Benutzer ist es bedauerlich, dass er sich mit der Installation von Drittanbieterpaketen auseinandersetzen muss, nur um den Durchschnitt einer Liste von Zahlen zu berechnen.

Dies führt zu Option Nummer 2, DIY-Statistikfunktionen. Auf den ersten Blick scheint dies eine attraktive Option zu sein, aufgrund der scheinbaren Einfachheit gängiger Statistikfunktionen. Zum Beispiel:

def mean(data):
    return sum(data)/len(data)

def variance(data):
    # Use the Computational Formula for Variance.
    n = len(data)
    ss = sum(x**2 for x in data) - (sum(data)**2)/n
    return ss/(n-1)

def standard_deviation(data):
    return math.sqrt(variance(data))

Das obige scheint bei einem zufälligen Test korrekt zu sein.

>>> data = [1, 2, 4, 5, 8]
>>> variance(data)
7.5

Aber das Hinzufügen einer Konstanten zu jedem Datenpunkt sollte die Varianz nicht verändern.

>>> data = [x+1e12 for x in data]
>>> variance(data)
0.0

Und Varianz sollte *niemals* negativ sein.

>>> variance(data*100)
-1239429440.1282566

Im Gegensatz dazu liefert die vorgeschlagene Referenzimplementierung für die ersten beiden Beispiele exakt das richtige Ergebnis 7,5 und für das dritte ein einigermaßen genaues Ergebnis: 6,012. Numpy macht es nicht besser [6].

Selbst einfache statistische Berechnungen bergen Fallen für Unachtsame, beginnend mit der Rechenformel selbst. Trotz des Namens ist sie numerisch instabil und kann, wie oben zu sehen, extrem ungenau sein. Sie ist für die Berechnung per Computer völlig ungeeignet [7]. Dieses Problem plagt Benutzer vieler Programmiersprachen, nicht nur Python [8], da Programmierer immer wieder denselben numerisch ungenauen Code neu erfinden [9] oder andere dazu raten [10].

Es sind nicht nur die Varianz und die Standardabweichung. Selbst der Mittelwert ist nicht so einfach, wie er erscheinen mag. Die obige Implementierung scheint zu einfach zu sein, um Probleme zu haben, aber das tut sie:

  • Die eingebaute Funktion sum kann Genauigkeit verlieren, wenn mit Gleitkommazahlen von stark unterschiedlicher Größenordnung umgegangen wird. Folglich schlägt der obige naive mean diesen „Härtetest“ fehl.
    assert mean([1e30, 1, 3, -1e30]) == 1
    

    Er gibt 0 statt 1 zurück, ein rein rechnerischer Fehler von 100%.

  • Die Verwendung von math.fsum innerhalb von mean macht sie bei Gleitkommadaten genauer, hat aber auch den Nebeneffekt, dass alle Argumente zu Gleitkommazahlen konvertiert werden, auch wenn dies unnötig ist. Z.B. sollten wir erwarten, dass der Mittelwert einer Liste von Brüchen ein Bruch ist, keine Gleitkommazahl.

Während die obige Mittelwertimplementierung nicht ganz so katastrophal fehlschlägt wie die naive Varianz, kann eine Standardbibliotheksfunktion viel besser sein als die DIY-Versionen.

Das obige Beispiel betrifft einen besonders schlechten Datensatz, aber auch für realistischere Datensätze ist Genauigkeit wichtig. Der erste Schritt bei der Interpretation von Variationen in Daten (einschließlich des Umgangs mit schlecht konditionierten Daten) ist oft die Standardisierung auf eine Reihe mit Varianz 1 (und oft Mittelwert 0). Diese Standardisierung erfordert eine genaue Berechnung des Mittelwerts und der Varianz der Rohreihe. Eine naive Berechnung von Mittelwert und Varianz kann sehr schnell Präzision verlieren. Da Präzision Genauigkeit begrenzt, ist es wichtig, die präzisesten praktisch anwendbaren Algorithmen zur Berechnung von Mittelwert und Varianz zu verwenden, oder die Ergebnisse der Standardisierung sind selbst nutzlos.

Vergleich mit anderen Sprachen/Paketen

Die vorgeschlagene Statistikbibliothek ist nicht als Konkurrenz zu Drittanbieterbibliotheken wie numpy/scipy oder zu proprietären, voll ausgestatteten Statistikpaketen für professionelle Statistiker wie Minitab, SAS und Matlab gedacht. Sie richtet sich an das Niveau von Grafik- und wissenschaftlichen Taschenrechnern.

Die meisten Programmiersprachen haben wenig oder gar keine integrierte Unterstützung für Statistikfunktionen. Einige Ausnahmen:

R

R (und sein proprietärer Ableger S) ist eine Programmiersprache, die für statistische Arbeiten entwickelt wurde. Sie ist bei Statistikern extrem beliebt und sehr funktionsreich [11].

C#

Das C#-LINQ-Paket enthält Erweiterungsmethoden zur Berechnung des Durchschnitts von Enumerables [12].

Ruby

Ruby wird nicht mit einem Standard-Statistikmodul ausgeliefert, obwohl es eine gewisse Nachfrage zu geben scheint [13]. Statsample scheint eine funktionsreiche Drittanbieterbibliothek zu sein, die darauf abzielt, mit R zu konkurrieren [14].

PHP

PHP verfügt über eine extrem funktionsreiche (wenn auch größtenteils undokumentierte) Sammlung fortschrittlicher Statistikfunktionen [15].

Delphi

Delphi enthält Standard-Statistikfunktionen, einschließlich Mittelwert, Summe, Varianz, Gesamtvarianz, Moment-Schiefe-Kurtosis in seiner Math-Bibliothek [16].

GNU Scientific Library

Die GNU Scientific Library enthält Standard-Statistikfunktionen, Perzentile, Median und andere [17]. Eine Innovation, die ich von der GSL übernommen habe, ist die Möglichkeit, dem Aufrufer optional den vordefinierten Mittelwert der Stichprobe (oder einen a priori bekannten Populationsmittelwert) bei der Berechnung von Varianz und Standardabweichung anzugeben [18].

Designentscheidungen des Moduls

Meine Absicht ist es, klein anzufangen und die Bibliothek nach Bedarf zu erweitern, anstatt zu versuchen, von Anfang an alles einzubauen. Daher enthält die aktuelle Referenzimplementierung nur eine kleine Anzahl von Funktionen: Mittelwert, Varianz, Standardabweichung, Median, Modus. (Siehe Referenzimplementierung für eine vollständige Liste.)

Ich habe folgende Designmerkmale angestrebt:

  • Korrektheit vor Geschwindigkeit. Es ist einfacher, eine korrekte, aber langsame Funktion zu beschleunigen, als eine schnelle, aber fehlerhafte zu korrigieren.
  • Konzentration auf Daten in Sequenzen, die zwei Durchläufe über die Daten erlauben, anstatt Genauigkeit zugunsten eines Ein-Pass-Algorithmus zu kompromittieren. Funktionen erwarten, dass Daten als Liste oder andere Sequenz übergeben werden; wenn ein Iterator übergeben wird, können sie intern in eine Liste konvertiert werden.
  • Funktionen sollten, soweit möglich, jeden numerischen Datentyp berücksichtigen. Z.B. sollte der Mittelwert einer Liste von Dezimalzahlen ein Dezimal sein, nicht eine Gleitkommazahl. Wenn dies nicht möglich ist, wird die Gleitkommazahl als „kleinster gemeinsamer Datentyp“ behandelt.
  • Obwohl Funktionen Datensätze von Gleitkommazahlen, Dezimalzahlen oder Brüchen unterstützen, gibt es keine Garantie, dass *gemischte* Datensätze unterstützt werden. (Aber andererseits werden sie auch nicht ausdrücklich abgelehnt.)
  • Viele Dokumentationen, die sich an Leser richten, die die Grundkonzepte verstehen, aber vielleicht nicht wissen (z.B.), welche Varianz sie verwenden sollen (Populations- oder Stichprobenvarianz?). Mathematiker und Statistiker haben die schreckliche Angewohnheit, inkonsistent mit Notation und Terminologie zu sein [19], und nachdem ich viele Stunden damit verbracht habe, die widersprüchlichen/verwirrenden Definitionen in Gebrauch zu verstehen, ist es nur fair, dass ich mein Bestes tue, das Thema zu klären und nicht zu verschleiern.
  • Aber vermeide es, in langwierige [20] mathematische Details einzudringen.

API

Die Anfangsversion der Bibliothek wird univariate (einvariable) Statistikfunktionen bereitstellen. Die allgemeine API basiert auf einem funktionalen Modell funktion(daten, ...) -> ergebnis, wobei daten ein obligatorisches iterierbares Objekt ist (normalerweise numerische Daten).

Der Autor erwartet, dass Listen der am häufigsten verwendete Datentyp sein werden, aber jeder iterierbare Typ sollte akzeptabel sein. Wo nötig, können Funktionen intern in Listen konvertiert werden. Wo möglich, wird erwartet, dass Funktionen den Datentyp der Werte beibehalten, z.B. sollte der Mittelwert einer Liste von Dezimalzahlen ein Dezimal sein und nicht eine Gleitkommazahl.

Berechnung von Mittelwert, Median und Modus

Die Funktionen mean, median* und mode nehmen ein einzelnes obligatorisches Argument entgegen und geben die entsprechende Statistik zurück, z.B.:

>>> mean([1, 2, 3])
2.0

Die bereitgestellten Funktionen sind:

  • mean(daten)
    arithmetischer Mittelwert von *Daten*.
  • median(daten)
    Median (mittlerer Wert) von *Daten*, wobei der Durchschnitt der beiden mittleren Werte genommen wird, wenn es eine gerade Anzahl von Werten gibt.
  • median_high(daten)
    Hoher Median von *Daten*, wobei der größere der beiden mittleren Werte genommen wird, wenn die Anzahl der Elemente gerade ist.
  • median_low(daten)
    Niedriger Median von *Daten*, wobei der kleinere der beiden mittleren Werte genommen wird, wenn die Anzahl der Elemente gerade ist.
  • median_grouped(daten, intervall=1)
    50. Perzentil von gruppierten *Daten*, unter Verwendung von Interpolation.
  • mode(daten)
    häufigster *Datenpunkt*.

mode ist die einzige Ausnahme von der Regel, dass das Datenargument numerisch sein muss. Es akzeptiert auch ein iterierbares Objekt mit nominalen Daten, wie z.B. Zeichenketten.

Berechnung von Varianz und Standardabweichung

Um wissenschaftlichen Taschenrechnern zu ähneln, wird das Statistikmodul separate Funktionen für Populations- und Stichprobenvarianz sowie Standardabweichung enthalten. Alle vier Funktionen haben ähnliche Signaturen, mit einem einzigen obligatorischen Argument, einem iterierbaren Objekt mit numerischen Daten, z.B.:

>>> variance([1, 2, 2, 2, 3])
0.5

Alle vier Funktionen akzeptieren auch ein zweites, optionales Argument, den Mittelwert der Daten. Dies ist nach einer ähnlichen API des GNU Scientific Library modelliert [18]. Es gibt drei Anwendungsfälle für die Verwendung dieses Arguments, in keiner bestimmten Reihenfolge:

  1. Der Wert des Mittelwerts ist *a priori* bekannt.
  2. Sie haben den Mittelwert bereits berechnet und möchten ihn nicht erneut berechnen.
  3. Sie möchten die Varianzfunktionen nutzen (missbrauchen), um das zweite Moment um einen gegebenen Punkt außer dem Mittelwert zu berechnen.

In jedem Fall liegt es in der Verantwortung des Aufrufers, sicherzustellen, dass das gegebene Argument sinnvoll ist.

Die bereitgestellten Funktionen sind:

  • variance(daten, xbar=None)
    Stichprobenvarianz von *Daten*, optional unter Verwendung von *xbar* als Stichprobenmittelwert.
  • stdev(daten, xbar=None)
    Stichprobenstandardabweichung von *Daten*, optional unter Verwendung von *xbar* als Stichprobenmittelwert.
  • pvariance(daten, mu=None)
    Populationsvarianz von *Daten*, optional unter Verwendung von *mu* als Populationsmittelwert.
  • pstdev(daten, mu=None)
    Populationsstandardabweichung von *Daten*, optional unter Verwendung von *mu* als Populationsmittelwert.

Andere Funktionen

Es gibt eine weitere öffentliche Funktion:

  • sum(daten, start=0)
    hochpräzise Summe von numerischen *Daten*.

Spezifikation

Da die vorgeschlagene Referenzimplementierung in reinem Python geschrieben ist, können andere Python-Implementierungen das Modul problemlos unverändert nutzen oder es nach Bedarf anpassen.

Wie soll das Modul heißen?

Dies wird ein Top-Level-Modul statistics sein.

Es gab ein gewisses Interesse daran, math in ein Paket umzuwandeln und dies zu einem Untermodul von math zu machen, aber der allgemeine Konsens einigte sich schließlich auf ein Top-Level-Modul. Andere mögliche, aber abgelehnte Namen waren stats (zu viel Verwechslungsgefahr mit dem bestehenden stat-Modul) und statslib (als „zu C-ähnlich“ beschrieben).

Diskussion und gelöste Probleme

Dieser Vorschlag wurde bereits hier diskutiert [21].

Eine Reihe von Designfragen wurden während der Diskussion auf Python-Ideas und der anfänglichen Code-Überprüfung gelöst. Es gab viel Besorgnis über die Hinzufügung einer weiteren sum-Funktion zur Standardbibliothek, siehe FAQs unten für weitere Details. Darüber hinaus litt die anfängliche Implementierung von sum unter einigen Rundungsfehlern und anderen Designproblemen beim Umgang mit Dezimalzahlen. Die Hilfe von Oscar Benjamin bei der Lösung dieses Problems war von unschätzbarem Wert.

Ein weiteres Problem war die Handhabung von Daten in Form von Iteratoren. Die erste Implementierung der Varianz schaltete lautlos zwischen einem Ein- und Zwei-Pass-Algorithmus um, je nachdem, ob die Daten in Form eines Iterators oder einer Sequenz vorlagen. Dies erwies sich als Designfehler, da die berechnete Varianz leicht je nach verwendetem Algorithmus abweichen konnte, und variance etc. wurden geändert, um intern eine Liste zu generieren und immer die genauere Zwei-Pass-Implementierung zu verwenden.

Ein kontroverses Design betraf die Funktionen zur Berechnung des Medians, die als Attribute des median-Aufrufs implementiert wurden, z.B. median, median.low, median.high etc. Obwohl es mindestens einen bestehenden Verwendungsfall dieses Stils in der Standardbibliothek gibt, in unittest.mock, hielten die Code-Rezensenten dies für die Standardbibliothek zu ungewöhnlich. Folglich wurde das Design zu einem traditionelleren Design mit separaten Funktionen und einer Pseudo-Namenskonvention geändert: median_low, median_high, etc.

Ein weiterer Punkt, der Code-Rezensenten besorgte, war die Existenz einer Funktion zur Berechnung des Stichprobenmodus für kontinuierliche Daten, wobei einige Leute die Wahl des Algorithmus in Frage stellten und ob es sich um einen ausreichend häufigen Bedarf handelte, um ihn einzubeziehen. Daher wurde er aus der API gestrichen, und mode implementiert nun nur noch den grundlegenden Schulbuchalgorithmus basierend auf dem Zählen eindeutiger Werte.

Ein weiterer wichtiger Diskussionspunkt war die Berechnung von Statistiken von timedelta-Objekten. Obwohl das Statistikmodul timedelta-Objekte nicht direkt unterstützt, ist es möglich, diesen Anwendungsfall zu unterstützen, indem man sie zuerst mithilfe der Methode timedelta.total_seconds in Zahlen umwandelt.

Häufig gestellte Fragen

Sollte dieses Modul nicht erst eine Weile auf PyPI verweilen, bevor es für die Standardbibliothek in Betracht gezogen wird?

Ältere Versionen dieses Moduls sind seit 2010 auf PyPI verfügbar [22]. Da es einfacher als numpy ist, erfordert es keine jahrelange externe Entwicklung.

Braucht die Standardbibliothek wirklich noch eine weitere Version von sum?

Dies erwies sich als der kontroverseste Teil der Referenzimplementierung. In einem Sinne sind eindeutig drei Summen zwei zu viel. Aber in einem anderen Sinne, ja. Die Gründe, warum die beiden bestehenden Versionen ungeeignet sind, sind hier beschrieben [23], aber die kurze Zusammenfassung lautet:

  • Die eingebaute Summe kann bei Gleitkommazahlen an Präzision verlieren;
  • die eingebaute Summe akzeptiert jeden nicht-numerischen Datentyp, der den Operator + unterstützt, mit Ausnahme von Zeichenketten und Bytes;
  • math.fsum ist hochpräzise, zwingt aber alle Argumente zu Gleitkommazahlen.

Es gab ein gewisses Interesse daran, eine der bestehenden Summen zu „reparieren“. Wenn dies vor dem Feature-Freeze von 3.4 geschieht, kann die Entscheidung, statistics.sum beizubehalten, neu überdacht werden.

Wird dieses Modul auf ältere Python-Versionen zurückportiert?

Das Modul zielt derzeit auf 3.3 ab, und ich werde es auf PyPI für 3.3 auf absehbare Zeit verfügbar machen. Das Zurückportieren auf ältere Versionen der 3.x-Serie ist wahrscheinlich (aber noch nicht entschieden). Das Zurückportieren auf 2.7 ist weniger wahrscheinlich, aber nicht ausgeschlossen.

Soll dies numpy ersetzen?

Nein. Obwohl es wahrscheinlich über die Jahre wachsen wird (siehe offene Punkte unten), ist es nicht darauf ausgelegt, numpy zu ersetzen oder gar direkt damit zu konkurrieren. Numpy ist eine voll ausgestattete numerische Bibliothek für Profis, der Kern des Ökosystems numerischer Bibliotheken in Python. Dies ist nur eine Batterie, im Sinne von „Batteries Included“, und richtet sich an ein Zwischenniveau zwischen „numpy verwenden“ und „eigene Version entwickeln“.

Zukünftige Arbeit

  • Zu diesem Zeitpunkt bin ich mir der besten API für multivariate Statistikfunktionen wie lineare Regression, Korrelationskoeffizient und Kovarianz unsicher. Mögliche APIs umfassen:
    • Separate Argumente für X- und Y-Daten
      function([x0, x1, ...], [y0, y1, ...])
      
    • Ein einzelnes Argument für (X, Y)-Daten
      function([(x0, y0), (x1, y1), ...])
      

      Diese API wird von GvR bevorzugt [24].

    • Auswahl beliebiger Spalten aus einem 2D-Array
      function([[a0, x0, y0, z0], [a1, x1, y1, z1], ...], x=1, y=2)
      
    • Eine Kombination der oben genannten.

    Mangels eines Konsenses über die bevorzugte API für multivariate Statistiken werde ich die Aufnahme solcher multivariaten Funktionen auf Python 3.5 verschieben.

  • Ebenso werden Funktionen zur Berechnung der Wahrscheinlichkeit von Zufallsvariablen und Inferenztests (z.B. Student’s t-Test) auf 3.5 verschoben.
  • Es gibt erhebliches Interesse an der Aufnahme von Ein-Pass-Funktionen, die mehrere Statistiken aus Daten in Iteratorform berechnen können, ohne sie in eine Liste konvertieren zu müssen. Das experimentelle stats-Paket auf PyPI enthält Coroutinen-Versionen von Statistikfunktionen. Die Aufnahme dieser wird auf 3.5 verschoben.

Referenzen


Quelle: https://github.com/python/peps/blob/main/peps/pep-0450.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT