Teorema funzionale del limite centrale

Il teorema del limite centrale funzionale, detto anche teorema di Donsker, è un’estensione funzionale del classico teorema del limite centrale (TLC). Il teorema del limite centrale è uno dei teoremi più importanti della teoria delle probabilità. In verità esso costituisce una classe di teoremi, di cui una delle formulazioni più note è la seguente:

Sia X_j una delle n variabili aleatorie indipendenti e identicamente distribuite, e siano E[X_j] = \mu e Var[X_j] = \sigma^2  \  \forall j, con 0 < \sigma^2 < +\infty.

Posto Y_n = \frac{\sum_{ j = 1}^n X_j - n \mu}{\sigma \sqrt{n}} allora Y_n presenterà una distribuzione normale standard.

In altre parole, dato un numero sufficiente (solitamente almeno 25 o 30) di osservazioni indipendenti generate da una stessa variabile aleatoria, se si considera la standardizzazione della somma di queste variabili, ovvero si toglie, dalla somma, n volte la media teorica e si divide per la deviazione standard teorica diviso la radice della numerosità campionaria, questa si distribuirà come una normale standard.

Il teorema di Donsker, in maniera simile, estende i risultati del limite centrale. Consideriamo infatti una successione di variabili aleatorie indipendenti e identicamente distribuite X_1, x_2, X_3,... e supponiamo, per comodità, che abbiano media 0 e varianza unitaria. Sia S_n una random walk definita come S_n = \sum_{i = 1}^{n} X_i Definiamo ora la random walk diffusa riscalata, definita da:

W^{(n)}(t) = \frac{S_{\lfloor n t \rfloor}}{\sqrt{n}}, \hspace{3ex} t \in [0,1]

Il TLC afferma che W^{(n)}(1) converge in distribuzione ad una normale standard. Il teorema di Donsker estende questo risultato, con il principio di invarianza, a tutta la funzione W^{(n)}, dimostrando che essa si distribuisce come un moto Browniano standard per n che tende ad infinito.

Processo aleatorio e Random walk

Un processo aleatorio (o processo stocastico) è un insieme ordinato di funzioni reali di un certo parametro (in genere il tempo) che gode di determinate proprietà statistiche. In generale è possibile identificare un processo stocastico come una famiglia ad un parametro di variabili casuali reali X(t) rappresentanti le trasformazioni dello stato iniziale nello stato al tempo t . In breve, un processo aleatorio è una successioni di variabili aleatorie, determinate da una stessa distribuzione di fondo, che, indicizzati da un parametro, permetto di modellare una successione di valori a partire da un valore iniziale. In ambito finanziario, permettono di modellare valori legati ai valori stessi in momenti precedenti, come l’andamento dei prezzi, le quotazioni in borsa, lo spread e così via.

Da un punto di vista astratto, qualsiasi strumento finanziario si può considerare come una traiettoria nel tempo, ovvero come un processo stocastico parametrizzato rispetto al tempo e che, in un certo istante t, assume un certo valore P(t) che può essere, ad esempio, il prezzo di quello stesso strumento al tempo t  (ovviamente, t  è una variabile aleatoria). In generale, i processi stocastici si suddividono in processi continui o processi discreti a seconda della natura di t .

In questo contesto, la random walk è un particolare tipo di processo aleatorio, che descrive un percorso dove gli spostamenti sono regolati da una legge probabilistica ben precisa. Nella passeggiata aleatoria monodimensionale, la probabilità di uno spostamento a destra è infatti associato ad una probabilità p (propria del fenomeno che si sta studiando), e analogamente la probabilità di uno spostamento a sinistra è associato ad una probabilità pari a (1-p). Ogni passo è di lunghezza uguale e indipendente agli altri. Si può subito notare che la previsione del valore della passeggiata aleatoria ad un determinato tempo  \hat{t} dipenderà esclusivamente dall’ultimo valore noto assunto dalla passeggiata aleatoria.

Spostandosi in ambito finanziario, questo processo può essere utilizzato, ad esempio, per studiare l’andamento delle quotazioni in borsa, anche se altri processi aleatori (come il moto Browniano, che è simile alla passeggiata aleatoria, me ne complica notevolmente le caratteristiche) sono più adeguati allo scopo.

Ricorsione e iterazione

I metodi ricorsivi ed i metodi iterativi sono di per sé simili, in quanto utilizzano entrambi utilizzo una struttura ripetitiva, caratterizzata da una condizione di uscita che termina l’esecuzione. La principale e fondamentale differenza, tuttavia, risiede nel fatto che un algoritmo iterativo presenta una struttura, quale un loop for, while o do-while, dove un’operazione viene eseguita e conclusa finché la condizione di uscita viene raggiunta. Un algoritmo ricorsivo, invece, richiama se stesso fino al raggiungimento della condizione di uscita, generando uno stack di chiamate che verrà chiuso a cascata, a partire dall’ultima chiamata generata, solo una volta che sarà raggiunta la condizione di uscita.

In generale, un algoritmo iterativo risulta spesso più leggibile, più facile da implementare e tendenzialmente meno incline a generare errori (anche grazie al meno peso che comporta sulla memoria). Di contro, però, gli algoritmi ricorsivi sono particolarmente adatti per risolvere determinati problemi, fornendo soluzioni semplici e facilmente implementabili, laddove altri algoritmi risultano eccessivamente complicati o addirittura impossibili da implementare.

Un esempio di algoritmo iterativo è il bubble sort (quasi tutti i sort sono algoritmi iterativi), mentre un esempio di algoritmo ricorsivo è il merge sort.

Uso della memoria da parte di un programma

Quando un programma viene avviato, viene messa a disposizione, da parte del computer, una certa quantità di memoria. Questa viene allocata principalmente per 4 diverse componenti:

  • una prima area di memoria è dedicata a contenere le istruzioni stesse del programma;
  • una seconda area di memoria è dedicata alle variabili che non sono definite nei metodi, ma che appartengono alla classe intera e servono generalmente per l’esecuzione del programma, le cosiddette variabili globali;
  • un’area di memoria, denominata heap, viene dedicata a tutti gli oggetti che non occupano una quantità di memoria definita, ma invece necessitano di una quantità di memoria dinamica, come ad esempio i reference type;
  • un’ultima area di memoria, denominata stack, viene dedicata alle informazioni relative alle chiamate e ai value type che non ricadono nelle variabili globali, ovvero quelle variabili proprie dei metodi (e non genericamente della classe stessa) che necessitano di una quantità ben definita di memoria.

Vedendo nel dettaglio l’area di memoria stack, essa è composta da diversi “settori” denominati stack frame. Ad ogni chiamata di un metodo viene istanziato uno stack frame dedicato, dove vengono registrate tutte le informazioni:

  • variabili value type istanziate in quel metodo;
  • parametri utilizzati;
  • indirizzo di ritorno al metodo chiamante.

La gestione degli eventi nella memoria stack, ovvero dell’aggiunta e dell’eliminazione degli stack frame, avviene secondo lo schema “last in, first out” che sostanzialmente consiste nell’esecuzione degli stack frame partendo dalla chiamata più recente per tornare poi a ritroso al primo metodo chiamato (solitamente il main), in maniera uguale e contraria a come avvengono le chiamate nell’esecuzione effettiva del programma.

Se un programma eccede la dimensione massima consentita (che è pari a 1 GB nei programmi in windows a 32 bit e a 4 GB nei programmi in windows a 64 bit), ad esempio quando viene chiamato un programma ricorsiva un numero eccessivo di volte, viene prodotta una StackOverflowException ed il programma viene, di conseguenza, interrotto.

Trasformazione di coordinate: dal “mondo reale” al “mondo virtuale”

Uno degli aspetti fondamentali quando si disegna un grafico attraverso C# o VB.NET è il riuscire a trasformare in maniera opportuna le coordinate del dataset che si vuole rappresentare. Infatti, una trasformazione delle coordinate errata può portare dei risultati sbagliati e fuorvianti. La prima cosa che bisogna ricordare è che il sistema richiede le coordinate dei punti in forma di pixel. In particolare, se si vuole riprodurre un certo punto determinato dal valore di due coordinate X e Y sullo schermo, bisognerà ottenere il suo equivalente grafico, dato dalle trasformazioni delle due variabili in coordinate pixel e con riferimento l’origine della bitmap istanziata in precedenza.

Questa trasformazione è di per sé molto semplice, ma richiede molta attenzione in quanto fondamentale per una giusta rappresentazione. Supponendo di avere un dataset in due variabili, X e Y, e supponendo di aver creato una bitmap caratterizzate dalle seguente variabili:

  • Left, ovvero la coordinata X del vertice in alto a sinistra del rettangolo;
  • Top, ovvero la coordinata Y del vertice in alto a sinistra del rettangolo;
  • Width, ovvero l’ampiezza del rettangolo;
  • Height, ovvero l’altezza del rettangolo;

allora occorrerà ottenere una coppia di coordinate, \hat{X} e \hat{Y}, che facciano rifermento alla bitmap, in particolare all’estremo sinistro, di cui possediamo le coordinate. Per ogni punto basterà, quindi, applicare queste trasformazioni:

La X può essere trasformata in maniera tale che ricada all’interno della bitmap. Servirà quindi che il massimo valore delle X sia associato all’estremo destro della bitmap e il minimo all’estremo sinistro, scalando di conseguenza tutti gli altri valori in maniera che risultino compresi tra questi due. Questo è ottenibile, per una generica osservazione i-esima, attraverso la seguente formula:

\hat{x}_i = \mathrm{Left} + \mathrm{Width} \cdot \frac{x_i - min(X)}{max(X)-min(X)}

Analogamente, bisognerà scalare in modo uguale anche le coordinate della Y, ricordando però che la bitmap fornisce un riferimento per l’estremo sinistro superiore, e non, come convenzionalmente è abitudine nel definire l’origine di un grafico, l’estremo sinistro inferiore. La formula risulterà essere la seguente:

\hat{y}_i = \mathrm{Top} +\mathrm{Height} \cdot( 1 - \frac{y_i - min(Y)}{max(Y)-min(Y)})

Principali oggetti grafici

Uno degli aspetti dove Visual Studio eccelle è nella gestione dell’interfaccia grafica. Infatti, sia VB.NET che C# mettono a disposizione una libreria, chiamata “GDI+“, che permette la rappresentazione di grafici (utile sopratutto per rappresentazioni statistiche), fornendo la possibilità di gestire diverse opzioni grafiche con molta facilità. La creazione di un grafico segue sostanzialmente alcuni step base, ovvero:

  1. creazione del bitmap, ovvero un’area di schermo (e conseguentemente di memoria, poiché verrà dapprima salvata e solo in seguito istanziata ) rettangolare caratterizzata da due variabili, altezza (height) e larghezza (width), che servirà per mostrare l’immagine successivamente creata;
  2. istanziazione dell’oggetto grafico graphics, ovvero l’istanziazione di GDI+ che permetterà in seguito di gestire e di creare il grafico stesso;
  3. esecuzione dei comandi per la creazione della parte grafica;
  4. display bitmap, ovvero rappresentazione del bitmap con un oggetto e stampa a schermo.

Il terzo step è quello che definisce il grafico in sé, andando a creare assi e oggetti che verranno poi disegnati. Bisogna perciò fare molta attenzione durante questo passaggio, in quanto un errore potrebbe comportare la creazione di un grafico completamente errato. Per la creazione del grafico esistono diversi oggetti che possono essere utilizzati, tra i quali:

  • Pen: oggetto che permette la creazione di linee, può essere manipolato con diverse opzioni grafiche quali:
    • Width, ovvero ampiezza del tratto;
    • Color, ovvero colore del tratto;
    • DashStyle, ovvero stile del tratto.
  • Brush: permette di riempire aree del grafico (la sua traduzione, di fatti, è pennello);
  • Point: permette la creazione di punti, determinati da due coordinate;
  • Rectangle: struttura “value type” che consente di disegnare un rettangolo attraverso 4 argomenti:
    • X, ovvero la coordinata X del vertice in alto a sinistra del rettangolo;
    • Y, ovvero la coordinata Y del vertice in alto a sinistra del rettangolo;
    • Width, ovvero l’ampiezza del rettangolo;
    • Height, ovvero l’altezza del rettangolo.
  • Font: consente di aggiungere un piccolo testo alle label del grafico e definisce uno specifico formato di testo;
  • Clear: metodo che serve per pulire l’intera area grafica, cancellando ogni istanza grafica.

Lambda expressions

In programmazione informatica, una funzione anonima o funzione lambda è una funzione o una subroutine definita, e possibilmente chiamata, senza essere legata ad un identificatore (da qui, appunto, funzione anonima ). Le funzioni lambda sono una forma di funzione nidificata, che consente l’accesso alle variabili nella portata della funzione contenitrice (variabili non locali).

Forniscono un scorciatoia rapida per applicare funzioni senza necessità di definire un nuovo metodo, o addirittura una nuova classe. Tuttavia, se un’espressione viene utilizzata in maniera ricorrente, è comunque preferibile costruire un metodo che possa essere chiamato ogni volta, senza dover ripete, ogni volta che risulta necessario, la definizione della funzione lambda.

Possono essere facilmente implementate sia in C#, sia in VB.NET. Vengono utilizzate spesso per la creazione di nuovi oggetti, ad esempio durante la gestione di un database, sopratutto se non relazionale, ma possono essere utilizzati anche per semplici trasformazioni. Alcuni esempi di questi utilizzi nei due linguaggi precedentemente citati sono i seguenti:

C#

// definizione di una lista di oggetti
List<Customer> custList = new List<Customer>
    {new Customer()
          { CustomerId = 1,
            FirstName="Bilbo",
            LastName = "Baggins",
            EmailAddress = "bb@hob.me"},
    new Customer()
          { CustomerId = 2,
            FirstName="Frodo",
            LastName = "Baggins",
            EmailAddress = "fb@hob.me"},
    new Customer()
          { CustomerId = 3,
            FirstName="Samwise",
            LastName = "Gamgee",
            EmailAddress = "sg@hob.me"},
    new Customer()
          { CustomerId = 4,
            FirstName="Rosie",
            LastName = "Cotton",
            EmailAddress = "rc@hob.me"}};
// definizione di una espressione lambda che opera sulla lista
var foundCustomer = custList.FirstOrDefault(c =>
                        c.CustomerId == 4);


// definizione di una espressione lambda che applica una funzione ad un oggetto (in questo caso la somma di due oggetti)

int add = (x, y) => { return x + y };
Console.WriteLine("{0}",add(1,2));

VB.NET

// definizione di una lista di oggetti
Dim custList As List(Of Customer) = New List(Of Customer) From {
    New Customer() With {
        .CustomerId = 1,
        .FirstName = "Bilbo",
        .LastName = "Baggins",
        .EmailAddress = "bb@hob.me"
    },
    New Customer() With {
        .CustomerId = 2,
        .FirstName = "Frodo",
        .LastName = "Baggins",
        .EmailAddress = "fb@hob.me"
    },
    New Customer() With {
        .CustomerId = 3,
        .FirstName = "Samwise",
        .LastName = "Gamgee",
        .EmailAddress = "sg@hob.me"
    },
    New Customer() With {
        .CustomerId = 4,
        .FirstName = "Rosie",
        .LastName = "Cotton",
        .EmailAddress = "rc@hob.me"
    }
}
// definizione di una espressione lambda che opera sulla lista
Dim foundCustomer = custList.FirstOrDefault(Function(c) c.CustomerId = 4)


// definizione di una espressione lambda che applica una funzione ad un oggetto (in questo caso la somma di due oggetti)

Dim add = Function(x, y)
             Return x + y
          End Function

Procedure per calcolare la distribuzione di frequenze univariata o bivariata

Nell’analisi statistica, è spesso molto interessante studiare delle variabili dal punto di vista della distribuzione di frequenza. Questo rende spesso necessario (a meno di variabili con un numero fisso di valori che possono assumere) la creazione di intervalli in cui valutare la distribuzione di frequenza. Infatti, variabili continue potrebbero assumere tutte valori diversi, nonostante il dataset possa raggiungere dimensioni notevoli.

Il primo ostacolo di questa analisi è la determinazione stessa dell’ampiezza degli intervalli. La scelta di questi è infatti fondamentale, poiché se troppo ridotto, l’intervallo diventa sostanzialmente inutile, riuscendo a contenere poche osservazioni, e se troppo ampio perde di significatività, andando ad accomunare variabili con valori profondamente diversi. Esistono anche determinazioni dinamiche di intervalli, in cui l’ampiezza varia a seconda del numero di osservazioni al loro interno. Tuttavia, una scelta semplice ed ottimale, che fissa le ampiezza tutte pari tra loro e che permette di la creazione di n classi, è data dalla formula:

W = \frac{max(X) - min(X)}{n}

dove X è la variabile presa in considerazione, n rappresenta il numero delle classi e W è l’ampiezza degli intervalli.

In questo modo è possibile costruire una tabella di frequenza. Nel caso univariato, ovvero nel caso in cui il numero di variabili prese in considerazione è soltanto uno, la tabella di distribuzione di frequenza risulta essere la seguente:

ClasseFrequenza
[x_0, x_1 ]N_1
(x_1, x_2 ]N_2
(x_n-1, x_n ]N_n

Una rapida implementazione di un algoritmo per la divisione in classi, una volta stabilita l’ampiezza dell’intervallo, consiste nell’ordinare le osservazioni e nel costruire via via le classi. Ovvero, si costruisce la prima classe, andando a selezionare come estremo inferiore l’osservazione minima e come estremo superiore la stessa osservazione sommata all’ampiezza dell’intervallo. In seguito, si scorrono le osservazioni e la prima osservazione che non rientra in questo intervallo, comporterà la creazione di un secondo intervallo. Questo viene ripetuto finché non sono state divise tutte le osservazioni.

Un altro caso interessante da studiare è il caso bivariato. In questa situazione, il numero di variabili considerato è pari a due. La divisione in classi sarà quindi leggermente diversa. Gli intervalli per le due variabili vengono costruiti separatamente con la stessa metodologia del caso univariato. Questi intervalli vengono però poi utilizzati per la costruzione di una matrice di distribuzione di frequenza, ovvero per valutare come le osservazioni si distribuiscano su tutte le possibili combinazioni di classi delle variabili X e Y. Denominando con C^k_x e C^k_y rispettivamente le classi k-esime delle variabili X e Y, la matrice di frequenza (chiamata anche tabella di contingenza) risulta essere:

X\YC^1_yC^2_yC^k_yC^n_y
C^1_xN_{11} N_{12}N_{1k}N_{1n}
C^2_xN_{21}N_{22}N_{2k}N_{2n}
C^k_xN_{k1}N_{k2}N_{kk}N_{kn}
C^n_xN_{n1}N_{n2}N_{nk}N_{nn}

Metodi disponibili per leggere file testuali di dati statistici, con esempi

In C# e VB.NET esistono diversi metodi per leggere i file di testo, da cui è possibile ottenere gli stessi risultati attraverso specificazioni diverse. Rimane discrezione del programmatore decidere quale sia il metodo più adatto nelle diverse situazioni. I tre metodi principali sono:

  • Hardcode;
  • StreamerReader;
  • TextFieldParse

Il metodo hardcode è sicuramente quello che presenta la sintassi più semplice tra tutti. Tuttavia, questa semplicità viene guadagnata a scapito di una perdita di ottimalità e precisione, nonché di leggibilità stessa. Il metodo hardcode consiste, sostanzialmente, nel definire una nuova variabile come una stringa che si inizializza specificando il percorso del file in questione (ovviamente tra apici). Di seguito sono riportati un esempio in VB.NET:

VB.NET

Dim File As String = "C://File.csv"

Il metodo StreamerReader risulta decisamente più comodo e versatile rispetto al precedente. Infatti, fornisce sia la possibilità di caricare un file direttamente da sistema, attraverso una “open file dialog” e quindi non rende necessario modificare ogni volta direttamente il codice nel caso si volesse utilizzare lo stesso programma per un set di dati diverso, sia la possibilità di caricare il file in maniera hardcoded, ovvero definendolo direttamente all’interno del codice girato e semplificato quindi, ad esempio, un’esecuzione ripetuta di un programma utilizzato su uno specifico dataset. Un esempio della versione che utilizza la versione dinamica in VB.NET e C# è la seguente:

VB.NET

'ottenimento della path del file attraverso una Open File Dialog
Dim ofd As New OpenFileDialog
ofd.ShowDialog()
 
'istanziamento della path in un oggetto
Dim sr As New System.IO.StreamReader(ofd.FileName)
 
'lettura delle righe del file e estrazione dei dati
Dim sr As New StreamReader(ofd.FileName)
Dim RigaH As String = sr.ReadLine()   'in questo modo legge l'header
While Not sr.EndOfStream
 Dim RigaDati As String = sr.ReadLine()
 Dim Dati() As String = RigaDati.Split(";".ToCharArray,StringSplitOptions.RemoveEmptyEntries)
End While

C#

            using (StreamReader sr = new StreamReader("TestFile.txt"))
            {
                string line;
                // Read and display lines from the file until the end of
                // the file is reached.
                while ((line = sr.ReadLine()) != null)
                {
                    Console.WriteLine(line);
                }
            }

L’ultimo metodo considerato è il TextFieldParse. Nonostante questo metodo risulti essere meno efficiente rispetto al StreamerReader, viene comunque spesso utilizzato per la lettura di file con estensione .csv. La principale differenza rispetto al metodo precedente sta nel fatto che, in VB.NET, questo metodo risulta già definito e non è quindi richiesta alcuna reference. Inoltre la sintassi rimane pressoché invariata, con l’eccezione dell’inizializzazione del percorso. Infatti, avviene una sostituzione del comando split e una eliminazione (o sostituzione) dei delimitatori. Un esempio di codice di questo metodo in VB.NET è il seguente:

VB.NET

Using MyReader As New Microsoft.VisualBasic.FileIO.
    TextFieldParser("c:\logs\bigfile")

    MyReader.TextFieldType = 
        Microsoft.VisualBasic.FileIO.FieldType.Delimited
    MyReader.Delimiters = New String() {vbTab}
    Dim currentRow As String()
    'Loop through all of the fields in the file. 
    'If any lines are corrupt, report an error and continue parsing. 
    While Not MyReader.EndOfData
        Try
            currentRow = MyReader.ReadFields()
            ' Include code here to handle the row.
        Catch ex As Microsoft.VisualBasic.FileIO.MalformedLineException
            MsgBox("Line " & ex.Message & 
            " is invalid.  Skipping")
        End Try
    End While
End Using

Elementi fondamentali della specifica CSV

In programmazione ed in statistica è fondamentale saper lavorare con dataset esterni. La prima cosa necessaria è saper importare correttamente diversi tipi di file all’interno del sistema in cui si sta lavorando. La maggior parte dei file contenenti dataset viene salvato attraverso l’estensione .csv.

Il CSV, abbreviazione di comma-separated values, è un formato di file basato su file di testo utilizzato per l’importazione ed esportazione (ad esempio da fogli elettronici o database) di una tabella di dati. Non esiste uno standard formale che lo definisca, ma solo alcune prassi più o meno consolidate. Ogni riga della tabella è normalmente rappresentata da una linea di testo, composta da diverse componenti (campi, o colonne, se si fa riferimento alla costruzione matriciale di un dataset). La separazione delle componenti è uno degli aspetti cruciali della lettura di un file .csv. Non esiste, infatti, una vera convenzione su quale separatore indichi la fine di una componente e l’inizio della successiva, ed è essenziale riuscire a ottenere questa informazione. I separatori più utilizzati sono “,” e “;”, ma è possibile trovare anche file che utilizzano come separatori spazi, virgolette o apici stessi. Questo implica che solitamente non è consigliato l’utilizzo di un di questi caratteri nella creazione dei record di un dataset, in quanto può facilmente portare ad una divisione errata del dataset.