English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Analisi di dati esplorativi in Python (funzionale)

Ecco alcune tecniche per gestire l'estrazione dei file di log. Supponiamo di essere interessati a una versione Enterprise di Splunk. Possiamo esplorare i dati con Splunk. Oppure possiamo ottenere un estratto semplice e manipolare questi dati in Python.

Eseguire diversi esperimenti in Python sembra più efficace rispetto a tentare operazioni esplorative in Splunk. Principalmente perché possiamo fare qualsiasi cosa con i dati senza limitazioni. Possiamo creare modelli statistici molto complessi in un solo posto.

Teoricamente, possiamo fare molte esplorazioni in Splunk. Ha diverse funzionalità di report e analisi.

Ma...

L'uso di Splunk richiede l'ipotesi che sappiamo cosa stiamo cercando. In molti casi, non sappiamo cosa stiamo cercando: stiamo esplorando. Potrebbero esserci alcuni indizi che suggeriscono che alcune elaborazioni di RESTful API sono lente, ma non è tutto. Come continuiamo?

Il primo passo è ottenere i dati originali in formato CSV. Come fare?

Lettura dei dati originali

Inizieremo con alcune funzioni aggiuntive per avvolgere un oggetto CSV.DictReader.

I puristi orientati verso l'oggettività si opporrebbero a questa strategia. “Perché non estendere DictReader?” chiedono. Non ho una buona risposta. Prefiro il programmazione funzionale e l'ortogonalità dei componenti. Per un metodo puramente orientato all'oggetto, dobbiamo utilizzare implementazioni più complesse di combinazione.

Il nostro framework generale per la gestione dei log è questo.

with open("somefile.csv") as source:
rdr = csv.DictReader(source)

Questo ci permette di leggere estratti di Splunk in formato CSV. Possiamo iterare le righe del lettore. Questo è il trucco numero 1. Non è molto complicato, ma mi piace.

with open("somefile.csv") as source:
rdr = csv.DictReader(source)
for row in rdr:
print( "{host} {ResponseTime} {source} {Service}".format_map(row) )

Possiamo - in una certa misura - riportare i dati originali in un formato utile. Se vogliamo abbellire l'output, possiamo modificare la stringa di formato. Potrebbe essere qualcosa come “{host:30s} {ResponseTime:8s} {source:s}” o simile.

Filtraggio

Una situazione comune è che estraiamo troppi dati, ma in realtà è necessario guardare solo un sottoinsieme. Possiamo modificare il filtro Splunk, ma, prima di completare la nostra esplorazione, l'eccessivo uso dei filtri è fastidioso. È molto più facile filtrare in Python. Una volta che sappiamo cosa serve, possiamo completare in Splunk.

with open("somefile.csv") as source:
rdr = csv.DictReader(source)
rdr_perf_log = (row for row in rdr if row['source'] == 'perf_log')
for row in rdr_perf_log:
print( "{host} {ResponseTime} {Service}".format_map(row))

Abbiamo aggiunto un'espressione generatrice per filtrare le righe di origine, in grado di gestire un sottoinsieme significativo.

Proiezione

In alcuni casi, aggiungiamo ulteriori colonne di dati di origine che non vogliamo utilizzare. Pertanto, eliminiamo questi dati attraverso la proiezione di ogni riga.

Principaliamente, Splunk non genera colonne vuote. Tuttavia, i log dell'API RESTful possono causare che il set di dati contenga molte intestazioni di colonna, basate su una parte dell'URI della richiesta. Queste colonne conterranno una riga di dati di una richiesta utilizzata da una chiave proxy. Per altre righe, in questa colonna non ha alcun utilizzo. Pertanto, è necessario eliminare queste colonne vuote.

Possiamo farlo anche con un'espressione generatrice, ma diventerà un po' lunga. La funzione generatrice è più facile da leggere.

def project(reader):
for row in reader:
yield {k:v for k,v in row.items() if v}

Abbiamo costruito una nuova tabella di riga da una parte del lettore originale. Possiamo usarlo per avvolgere l'output del nostro filtro.

with open("somefile.csv") as source:
rdr = csv.DictReader(source)
rdr_perf_log = (row for row in rdr if row['source'] == 'perf_log')
for row in project(rdr_perf_log):
print( "{host} {ResponseTime} {Service}".format_map(row))

Questo ridurrà le colonne non utilizzate visibili all'interno della statement for.

Cambiamenti di simbolo

Il simbolo row['source'] diventa un po' ingombrante. Usare types.SimpleNamespace è meglio che usare dizionari. Questo ci permette di usare row.source.

È un trucco molto cool per creare qualcosa di più utile.

rdr_ns= (types.SimpleNamespace(**row) forrowinreader)

Possiamo riassumere questi passaggi in una sequenza.

with open("somefile.csv") as source:
rdr = csv.DictReader(source)
rdr_perf_log = (row for row in rdr if row['source'] == 'perf_log')
rdr_proj = project(rdr_perf_log)
rdr_ns = (types.SimpleNamespace(**row) for row in rdr_proj)
for row in rdr_ns:
print( "{host} {ResponseTime} {Service}".format_map(vars(row)))

Notate la piccola modifica a format_map(). Dalla proprietà SimpleNamespace, abbiamo aggiunto la funzione vars() per estrarre il dizionario.

Possiamo scriverlo come funzione usando altre funzioni per mantenere la simmetria sintattica.

def ns_reader(reader):
return (types.SimpleNamespace(**row) for row in reader)

In effetti, possiamo scriverlo come una struttura lambda utilizzabile come funzione.

ns_reader = lambda reader: (types.SimpleNamespace(**row) for row in reader)

Anche se il funzionamento di ns_reader() e della lambda ns_reader() è lo stesso, è un po' più difficile scrivere la documentazione e i test unitari doctest per la lambda. Per questo motivo, è meglio evitare di usare la struttura lambda.

Possiamo usare map(lambda row: types.SimpleNamespace(**row), reader). Alcuni preferiscono questa espressione generatrice.

Possiamo usare una frase for appropriata e una frase yield interna, ma sembra che non ci sia alcun vantaggio nel scrivere frasi grandi da cose piccole.

Abbiamo molte scelte perché Python offre così tante funzionalità di programmazione funzionale. Anche se non vediamo spesso Python come un linguaggio funzionale, abbiamo molti modi per gestire semplici mappature.

Mappatura: trasformazione e dati derivati

Spesso abbiamo una lista di trasformazioni di dati molto chiara. Inoltre, avremo una lista crescente di progetti derivati. I progetti derivati saranno dinamici e basati su diverse ipotesi che stiamo testando. Ogni volta che abbiamo un esperimento o un problema, potremmo cambiare i dati derivati.

Ogni uno di questi passaggi: filtraggio, proiezione, conversione e derivazione sono fasi della parte "map" del pipeline map-reduce. Possiamo creare alcune funzioni più piccole e applicarle a map(). Poiché stiamo aggiornando un oggetto con stato, non possiamo utilizzare la funzione map() generale. Se vogliamo implementare uno stile di programmazione funzionale più puro, useremo un namedtuple immutabile invece di un SimpleNamespace mutabile.

def convert(reader):
for row in reader:
row._time = datetime.datetime.strptime(row.Time, "%Y-%m-%dT%H:%M:%S.%F%Z")
row.response_time = float(row.ResponseTime)
yield row

Nel nostro processo di esplorazione, ajusteremo il corpo della funzione di conversione. Potremmo iniziare con alcune trasformazioni e derivazioni minime. Continueremo con alcune domande come "Questi sono corretti?" per continuare l'esplorazione. Quando troviamo qualcosa che non funziona, lo rimuoveremo.

Il nostro processo di elaborazione complessivo è come segue:

with open("somefile.csv") as source:
rdr = csv.DictReader(source)
rdr_perf_log = (row for row in rdr if row['source'] == 'perf_log')
rdr_proj = project(rdr_perf_log)
rdr_ns = (types.SimpleNamespace(**row) for row in rdr_proj)
rdr_converted = convert(rdr_ns)
for row in rdr_converted:
row.start_time = row._time - datetime.timedelta(seconds=row.response_time)
row.service = some_mapping(row.Service)
print( "{host:30s} {start_time:%H:%M:%S} {response_time:6.3f} {service}".format_map(vars(row)) )

Attenzione al cambiamento del corpo della frase. La funzione convert() produce i valori che abbiamo determinato. Abbiamo aggiunto alcune variabili aggiuntive nel ciclo for, non possiamo essere del 100% certi. Prima di aggiornare la funzione convert(), vedremo se sono utili (o persino corrette).

Ridimensionamento

Nel ridimensionamento, possiamo adottare un metodo di lavorazione leggermente diverso. Dobbiamo ricostruire l'esempio precedente e trasformarlo in una funzione generatrice.

def converted_log(some_file):
with open(some_file) as source:
rdr = csv.DictReader(source)
rdr_perf_log = (row for row in rdr if row['source'] == 'perf_log')
rdr_proj = project(rdr_perf_log)
rdr_ns = (types.SimpleNamespace(**row) for row in rdr_proj)
rdr_converted = convert(rdr_ns)
for row in rdr_converted:
row.start_time = row._time - datetime.timedelta(seconds=row.response_time)
row.service = some_mapping(row.Service)
yield row

Poi abbiamo sostituito print() con un yield.

Questo è un'altra parte di rifattorizzazione.

for row in converted_log("somefile.csv"):
print( "{host:30s} {start_time:%H:%M:%S} {response_time:6.3f} {service}".format_map(vars(row)) )

Idealmente, tutto il nostro codice programmato dovrebbe essere così. Utilizziamo funzioni generatrici per generare dati. La visualizzazione finale dei dati rimane completamente separata. Questo ci permette di riflettere e cambiare la gestione con maggiore libertà.

Ora possiamo fare alcune cose, come raccogliere le righe in un oggetto Counter() o calcolare alcune informazioni statistiche. Possiamo raggruppare le righe per servizio utilizzando defaultdict(list).

by_service= defaultdict(list)
for row in converted_log("somefile.csv"):
by_service[row.service] = row.response_time
for svc in sorted(by_service):
m = statistics.mean( by_service[svc] )
print( "{svc:15s} {m:.2f}".format_map(vars()) )

Abbiamo deciso di creare oggetti elenco specifici qui. Possiamo utilizzare itertools per raggruppare le risposte per servizio. Sembra essere un'implementazione corretta di programmazione funzionale, ma questa implementazione indica alcune limitazioni nel formato di programmazione funzionale Pythonic. O dobbiamo ordinare i dati (creare oggetti elenco) o creare elenchi quando raggruppiamo i dati. Per fare diverse statistiche, è spesso più facile raggruppare i dati creando elenchi specifici.

Stiamo facendo due cose, non semplicemente stampare l'oggetto riga.

Creiamo alcune variabili locali come svc e m. Possiamo facilmente aggiungere variazioni o misure.

L'uso della funzione vars() senza parametri crea un dizionario dai variabili locali.

L'uso di vars() senza parametri è un trucco conveniente come locals(). Ci permette di creare semplicemente qualsiasi variabile locale desiderata e includerla nell'output formattato. Possiamo invadere vari metodi statistici che riteniamo possano essere correlati.

Poiché il nostro ciclo di elaborazione di base è diretto alle righe di converted_log(“somefile.csv”), possiamo esplorare molte opzioni di elaborazione attraverso uno script piccolo e modificabile. Possiamo esplorare alcune ipotesi per determinare il motivo per cui alcune elaborazioni RESTful API sono lente, mentre altre sono rapide.

Conclusione

Come menzionato sopra, l'editor ha introdotto l'analisi dati esplorativa in Python (funzionale) agli utenti, sperando che sia utile a tutti. Se avete domande, lasciate un commento e l'editor risponderà tempestivamente. In questo momento, desideriamo anche ringraziare tutti i sostenitori della guida a urla!

Dichiarazione: il contenuto di questo articolo è stato tratto da Internet, il diritto d'autore è della proprietà del rispettivo autore, il contenuto è stato contribuito e caricato autonomamente dagli utenti di Internet, questo sito non detiene il diritto di proprietà, non è stato editato manualmente e non assume alcuna responsabilità legale. Se trovi contenuti sospetti di violazione del copyright, ti preghiamo di inviare una e-mail a notice#oldtoolbag.com (al momento dell'invio dell'e-mail, sostituisci # con @) per segnalare il problema e fornire prove pertinenti. Una volta verificata, questo sito eliminerà immediatamente il contenuto sospetto di violazione del copyright.

Ti potrebbe interessare