Es muss auch kurze Blogposts geben! Nach meinem jüngsten Blog-Exzess über
Dominanzrelationen folgt hier ein Beitrag über ein vor längerer Zeit von Gerhard Lukert ersonnenes Verfahren, um Trendumkehrpunkte in Datenreihen zu ermitteln.
Es gibt Zahlenfolgen, die, wie die Börsenkurse an aufeinanderfolgenden Börsentagen, durch ihre Irregularität gekennzeichnet sind. Und zugleich kann man nach zugrundeliegenden Mustern, nach Trends, nach irgendwie um diese Irregularität "bereinigten" Informationen fragen. Im Fall der Börsenkurse leben Analysten davon, mit irgendwelchen Hilfslinien oder numerischen Verfahren aus den Daten Trends vorherzusagen.
Gerhard Lukerts Fragestellung war eine astrologische: ihn interessierten Tage, die besonders von einer Wende im Wachstumsverhalten geprägt sind. Solche Tage nennt er
Umkehrtage. Ein Verfahren, um solche Umkehrtage, die besonders markant die Trendwende in sich tragen, könnte nach Gerhard Lukert
eine nützliche Sache sein, weil die Umkehrtage signifikant "andere" Konstellationentypen haben müssten als die Trendfolgetage; es sind die eigentlich kritischen bzw. impulsgebenden Tage.
Sein Verfahren besteht darin, aus der ursprünglichen Zahlenfolge zwei neue Zahlenfolgen zu ermitteln – die Folgen der oberen und der unteren Umkehrpunkte, in denen sich jeweils der Trend umkehrt: bei den oberen Umkehrpunkten hören die Zahlen auf zu wachsen, bei den unteren Umkehrpunkten hören sie auf zu fallen.
Auf diese beiden Teilfolgen der oberen und unteren Umkehrpunkte kann dasselbe Verfahren jeweils noch einmal angewendet werden. Dabei ergeben sich vier neue Teilfolgen. Da ihn nur ein besonders reiner Ausdruck der Trendumkehr interessiert, behält er nur die oberen Umkehrpunkte der oberen und die unteren Umkehrpunkte der unteren Umkehrpunkte der vorherigen Iteration, so dass er auch in diesem Schritt mit zwei Teilfolgen verbleibt. Bei jedem Schritt dünnen sich die Folgen weiter aus, und die verbleibenden Punkte enthalten gewissermaßen besonders viel Essenz der Trendumkehr. Man verbleibt mit sehr wenigen Daten- (und damit in der Regel Zeit-)Punkten, die die Qualität dieser Umkehr besonders gut ausdrücken.
Auf der Webseite
http://ruediger-plantiko.net/filter/ kann man das Verfahren ausprobieren. Die Eingabe der Zahlenreihe kann aus einer Textdatei erfolgen oder über die Zwischenablage in das Eingabefeld. Mit dem Doppelkreuz
#
werden Kommentare eingeleitet, die beim Einlesen ignoriert werden, sie können auch am Ende einer Zeile stehen. Auch Leerzeilen werden ignoriert. Am Beginn der Zeile muss eine Zahl stehen, die von JavaScript als Zahl erkannt werden kann. Danach kann, von Leerzeichen oder einem Semikolon getrennt, ein Bezeichner folgen, der dann auch im Graphen angezeigt wird. Enthalten die Zeilen nur eine Zahl, so wird die Zeilennummer als Bezeichner verwendet.
Mit
Daten auswerten, oder den Buttons ◀ und ▶ zum Fortsetzen der Iterationen, können die Umkehrpunkte ermittelt und in einem Graphen zusammen mit der ursprünglichen Zahlenfolge angezeigt werden.
Die Anzeige des Graphen erfolgt mit
c3.js, einem auf Graphen spezialisierten Zusatz zu der bekannten Bibliothek
d3.js für
Data Driven Documents.
Hier ein Screenshot nach Datenauswertung:
Hier noch ein paar Bemerkungen zur Implementierung (in
filter.js). Beim Parsen der Eingabe werden zunächst die Kommentare und Leerzeilen entfernt. Danach wird aus jeder Datenzeile eine Zahl und ein nachfolgender Bezeichner eingelesen. Zusammen mit dem zur eindeutigen Benennung vorangestellten Index wird so ein Array von Arrays (AoA) erzeugt, wobei jedem Folgenelement ein vierelementiges Array zugeordnet ist: das erste Element erhält den Index, das zweite Element den Zahlenwert, das dritte den Bezeichner und das vierte das Wachstumsverhalten als Signum (also mit den Werten -1, 0 oder 1) im Vergleich zum Vorgänger.
function parseInput( stream ) {
const DATA_PATTERN = /^([-+.eE\d]+)(?:\s*|;)(.*)/;
const COMMENT_PATTERN = /\s*#.*$/;
var series = [];
stream.split('\n').forEach( function(line,i) {
try {
line = line.replace(COMMENT_PATTERN,"");
if (!line.match(/\S/)) return; // Leerzeilen überspringen
var pair = parseLine(line,i);
series.push( [ series.length, pair[0], pair[1] ] );
} catch (e) {
e.message += " (Zeile "+(i+1)+": '"+line+"')";
throw e;
}
});
return series.map( appendGrowth );
function parseLine( line, i ) {
var m = line.match(DATA_PATTERN);
if (m === null || m.length === 0) {
throw new Error("Zeile muss mit einer Zahl beginnen");
}
checkNumeric( m[1] );
return [ 1*m[1], m[2] || '#'+(i+1) ]
}
}
Hier ermittelt
appendGrowth
das
Signum der Datenänderung im Vergleich zum Vorgänger. Stimmt der Wert des Vorgängers mit dem aktuellen Wert überein, wird weiter zurückgeschaut, bis man einen echt größeren oder echt kleineren Wert gefunden hat. Die Funktionsschnittstelle entspricht dabei der Schnittstelle von Array-Iteratorfunktionen wie
map, so dass dieses Signum mit dem Aufruf
.map( appendGrowth )
den (vorher nur dreielementigen) Daten-Arrays hinzugefügt werden kann.
function appendGrowth(data,i,total) {
return data.concat( getGrowth( ) );
// Der Wert ist immer -1, 0 oder +1
function getGrowth( ) {
var sign = 0;
// Zurückspulen, bis ein echtes Zu- oder Abnehmen gefunden wurde
for (let j=i-1;j>=0&&sign===0;j--) {
sign = Math.sign( data[1] - total[j][1] );
}
return sign;
}
}
Die zentrale Funktion
getTurningPoints
ermittelt aus einer Zahlenfolge
series
die Folge ihrer Umkehrpunkte, und zwar je nach Funktion
condition
die Folge der oberen oder der unteren Umkehrpunkte. Hierzu wird, wie man vom Namen erwarten könnte, die JavaScript-Funktion
Array.prototype.filter
verwendet. Die Elemente der entstehenden Teilmenge, die ja vierelementige Arrays sind, werden dann kopiert, da das vierte Element, das Wachstumsverhalten, für jede Reihe neu ermittelt werden muss.
function getTurningPoints(series,condition) {
var newSeries =
series
.filter( conditionSatisfied )
.map( copy )
.map( appendGrowth );
return newSeries;
// Auf Umkehrpunkt prüfen (bis zum vorletzten Datenpunkt möglich)
function conditionSatisfied(data, i) {
return (i < series.length - 1) &&
condition(data[3],series[i+1][3])
}
// Kopie der ersten drei Elemente
function copy(data) {
return data.slice(0,3);
}
}
Aus dieser Funktion resultieren die Funktionen
getUpperTurningPoints
und
getLowerTurningPoints
, die sich nur durch die verwendete
condition
beim Aufruf von
getTurningPoints
unterscheiden:
function getUpperTurningPoints(series) {
return getTurningPoints( series, stopsIncreasing );
}
function getLowerTurningPoints(series) {
return getTurningPoints( series, stopsDecreasing );
}
function stopsIncreasing(currentGrowth,nextGrowth) {
return (currentGrowth>0) && (nextGrowth<0);
}
function stopsDecreasing(currentGrowth,nextGrowth) {
return (currentGrowth<0) && (nextGrowth>0);
}
Keine Kommentare :
Kommentar veröffentlichen