2016-03-11 9 views
5

Diciamo che ho un file di testo che assomiglia a questo:Come usare le parentesi quadre come carattere preventivo in Pandas.read_csv

Item,Date,Time,Location 
1,01/01/2016,13:41,[45.2344:-78.25453] 
2,01/03/2016,19:11,[43.3423:-79.23423,41.2342:-81242] 
3,01/10/2016,01:27,[51.2344:-86.24432] 

Quello che mi piacerebbe essere in grado di fare è leggere che con pandas.read_csv, ma la seconda riga genererà un errore. Ecco il codice Attualmente sto usando:

import pandas as pd 
df = pd.read_csv("path/to/file.txt", sep=",", dtype=str) 

Ho cercato di impostare quotechar a "[", ma che, ovviamente, mangia solo le linee fino alla prossima parentesi aperta e l'aggiunta di una parentesi di chiusura si traduce in una errore "stringa di lunghezza 2 trovata". Qualsiasi intuizione sarebbe molto apprezzata. Grazie!

Aggiornamento

C'erano tre soluzioni principali che sono stati offerti: 1) Dare una lunga serie di nomi per il frame di dati per permettere a tutti i dati da leggere e poi post-elaborare i dati, 2) trovare i valori tra parentesi quadre e tra virgolette, o 3) sostituire il primo n numero di virgole con il punto e virgola.

Nel complesso, non penso che l'opzione 3 sia una soluzione valida in generale (anche se va bene per i miei dati) perché a) cosa succede se ho valori quotati in una colonna che contengono virgole eb) cosa succede se la mia colonna con parentesi quadre non è l'ultima colonna? Ciò lascia le soluzioni 1 e 2. Penso che la soluzione 2 sia più leggibile, ma la soluzione 1 era più efficiente, in esecuzione in soli 1,38 secondi, rispetto alla soluzione 2, che funzionava in 3,02 secondi. I test sono stati eseguiti su un file di testo contenente 18 colonne e più di 208.000 righe.

+2

Nessuna delle implementazioni CSV che ho visto distinguono i caratteri di apri/chiudi citazione, che è il problema principale qui. La soluzione migliore è pre-elaborare il file e sostituire le parentesi con virgolette bilanciate. Questo può essere fatto semplicemente usando espressioni regolari (in Python o in uno strumento di streaming come 'awk'). –

+0

basta sostituire] su [, prima di passare a csv loader? – YOU

risposta

1

Penso che si possa replace primi 3 occorrenza di , in ogni riga del file da ; e quindi utilizzare il parametro sep=";" in read_csv:

import pandas as pd 
import io 

with open('file2.csv', 'r') as f: 
    lines = f.readlines() 
    fo = io.StringIO() 
    fo.writelines(u"" + line.replace(',',';', 3) for line in lines) 
    fo.seek(0)  

df = pd.read_csv(fo, sep=';') 
print df 
    Item  Date Time       Location 
0  1 01/01/2016 13:41     [45.2344:-78.25453] 
1  2 01/03/2016 19:11 [43.3423:-79.23423,41.2342:-81242] 
2  3 01/10/2016 01:27     [51.2344:-86.24432] 

Oppure può provare questo approccio complicato, perché problema principale è, separatore , tra i valori in lists corrisponde al separatore di altri valori di colonna.

quindi è necessario post - processing:

import pandas as pd 
import io 

temp=u"""Item,Date,Time,Location 
1,01/01/2016,13:41,[45.2344:-78.25453] 
2,01/03/2016,19:11,[43.3423:-79.23423,41.2342:-81242,41.2342:-81242] 
3,01/10/2016,01:27,[51.2344:-86.24432]""" 
#after testing replace io.StringIO(temp) to filename 
#estimated max number of columns 
df = pd.read_csv(io.StringIO(temp), names=range(10)) 
print df 
     0   1  2     3    4 \ 
0 Item  Date Time    Location    NaN 
1  1 01/01/2016 13:41 [45.2344:-78.25453]    NaN 
2  2 01/03/2016 19:11 [43.3423:-79.23423 41.2342:-81242 
3  3 01/10/2016 01:27 [51.2344:-86.24432]    NaN 

       5 6 7 8 9 
0    NaN NaN NaN NaN NaN 
1    NaN NaN NaN NaN NaN 
2 41.2342:-81242] NaN NaN NaN NaN 
3    NaN NaN NaN NaN NaN 
#remove column with all NaN 
df = df.dropna(how='all', axis=1) 
#first row get as columns names 
df.columns = df.iloc[0,:] 
#remove first row 
df = df[1:] 
#remove columns name 
df.columns.name = None 

#get position of column Location 
print df.columns.get_loc('Location') 
3 
#df1 with Location values 
df1 = df.iloc[:, df.columns.get_loc('Location'): ] 
print df1 
       Location    NaN    NaN 
1 [45.2344:-78.25453]    NaN    NaN 
2 [43.3423:-79.23423 41.2342:-81242 41.2342:-81242] 
3 [51.2344:-86.24432]    NaN    NaN 

#combine values to one column 
df['Location'] = df1.apply(lambda x : ', '.join([e for e in x if isinstance(e, basestring)]), axis=1) 

#subset of desired columns 
print df[['Item','Date','Time','Location']] 
    Item  Date Time           Location 
1 1 01/01/2016 13:41        [45.2344:-78.25453] 
2 2 01/03/2016 19:11 [43.3423:-79.23423, 41.2342:-81242, 41.2342:-8... 
3 3 01/10/2016 01:27        [51.2344:-86.24432] 
+0

Sì, è un altro modo. Ma se il separatore tra le colonne è altro come ',' ad es. ';' or '|', questa è un'altra soluzione. – jezrael

+0

Questo non è un approccio sbagliato, dato che nessuno dei dati, oltre alla posizione, dovrebbe contenere virgole. Prenderò in considerazione anche questo approccio. – brittenb

1

non riesco a pensare ad un modo per ingannare il parser CSV ad accettare distinte vicino caratteri aperti/quote, ma si può ottenere via con un piuttosto semplice passo di pre-elaborazione:

import pandas as pd 
import io 
import re 

# regular expression to capture contents of balanced brackets 
location_regex = re.compile(r'\[([^\[\]]+)\]') 

with open('path/to/file.txt', 'r') as fi: 
    # replaced brackets with quotes, pipe into file-like object 
    fo = io.StringIO() 
    fo.writelines(unicode(re.sub(location_regex, r'"\1"', line)) for line in fi) 

    # rewind file to the beginning 
    fo.seek(0) 

# read transformed CSV into data frame 
df = pd.read_csv(fo) 
print df 

Questo vi dà un risultato come

012.
  Date_Time Item        Location 
0 2016-01-01 13:41:00  1     [45.2344:-78.25453] 
1 2016-01-03 19:11:00  2 [43.3423:-79.23423, 41.2342:-81242] 
2 2016-01-10 01:27:00  3     [51.2344:-86.24432] 

Modifica Se la memoria non è un problema, allora si sta meglio pre-elaborazione dei dati in massa anziché linea per linea, come si fa in Max's answer.

# regular expression to capture contents of balanced brackets 
location_regex = re.compile(r'\[([^\[\]]+)\]', flags=re.M) 

with open('path/to/file.csv', 'r') as fi: 
    data = unicode(re.sub(location_regex, r'"\1"', fi.read())) 

df = pd.read_csv(io.StringIO(data)) 

Se si conosce in anticipo che le uniche parentesi nel documento sono quelli che circondano le coordinate di posizione, e che essi sono garantiti per essere equilibrato, allora è possibile semplificare ancora di più (Max suggerisce una linea-by versione-line di questo, ma penso l'iterazione è inutile):

with open('/path/to/file.csv', 'r') as fi: 
    data = unicode(fi.read().replace('[', '"').replace(']', '"') 

df = pd.read_csv(io.StringIO(data)) 

Questi sono i risultati di temporizzazione ho ottenuto con un 200k fila da 3 colonne di dati. Ogni volta viene calcolata una media di oltre 10 prove.

  • trama di dati di post-elaborazione (jezrael's solution): 2.19s
  • riga per riga regex: 1.36s
  • bulk regex: 0.39s
  • stringa bulk sostituire: 0.14 s
+0

Ricevo questo errore con il codice: 'TypeError: argomento unicode atteso, ottenuto 'str'' – brittenb

+0

Corretto, dovevo avvolgere le linee trasformate in' unicode' in modo che 'StringIO' non si lamentasse. Scusate, stavo testando questo con un setup leggermente diverso (usato 'StringIO' come input senza leggere il file dal disco). –

+0

Funziona, ma sfortunatamente è più lento dell'approccio originale di @jezrael. Il tempismo su questo approccio con 203.845 linee è di 3.04 secondi sulla mia macchina, rispetto a 1.38 secondi con Jezrael's. – brittenb

1

Possiamo usare semplice trucco - citazione parentesi quadre bilanciati con virgolette doppie:

import re 
import six 
import pandas as pd 


data = """\ 
Item,Date,Time,Location,junk 
1,01/01/2016,13:41,[45.2344:-78.25453],[aaaa,bbb] 
2,01/03/2016,19:11,[43.3423:-79.23423,41.2342:-81242],[0,1,2,3] 
3,01/10/2016,01:27,[51.2344:-86.24432],[12,13] 
4,01/30/2016,05:55,[51.2344:-86.24432,41.2342:-81242,55.5555:-81242],[45,55,65]""" 

print('{0:-^70}'.format('original data')) 
print(data) 
data = re.sub(r'(\[[^\]]*\])', r'"\1"', data, flags=re.M) 
print('{0:-^70}'.format('quoted data')) 
print(data) 
df = pd.read_csv(six.StringIO(data)) 
print('{0:-^70}'.format('data frame')) 

pd.set_option('display.expand_frame_repr', False) 
print(df) 

uscita:

----------------------------original data----------------------------- 
Item,Date,Time,Location,junk 
1,01/01/2016,13:41,[45.2344:-78.25453],[aaaa,bbb] 
2,01/03/2016,19:11,[43.3423:-79.23423,41.2342:-81242],[0,1,2,3] 
3,01/10/2016,01:27,[51.2344:-86.24432],[12,13] 
4,01/30/2016,05:55,[51.2344:-86.24432,41.2342:-81242,55.5555:-81242],[45,55,65] 
-----------------------------quoted data------------------------------ 
Item,Date,Time,Location,junk 
1,01/01/2016,13:41,"[45.2344:-78.25453]","[aaaa,bbb]" 
2,01/03/2016,19:11,"[43.3423:-79.23423,41.2342:-81242]","[0,1,2,3]" 
3,01/10/2016,01:27,"[51.2344:-86.24432]","[12,13]" 
4,01/30/2016,05:55,"[51.2344:-86.24432,41.2342:-81242,55.5555:-81242]","[45,55,65]" 
------------------------------data frame------------------------------ 
    Item  Date Time           Location  junk 
0  1 01/01/2016 13:41        [45.2344:-78.25453] [aaaa,bbb] 
1  2 01/03/2016 19:11     [43.3423:-79.23423,41.2342:-81242] [0,1,2,3] 
2  3 01/10/2016 01:27        [51.2344:-86.24432]  [12,13] 
3  4 01/30/2016 05:55 [51.2344:-86.24432,41.2342:-81242,55.5555:-81242] [45,55,65] 

UPDATE: se si è certi che tutte le parentesi quadre sono saldi, non dobbiamo usare RegEx di :

import io 
import pandas as pd 

with open('35948417.csv', 'r') as f: 
    fo = io.StringIO() 
    data = f.readlines() 
    fo.writelines(line.replace('[', '"[').replace(']', ']"') for line in data) 
    fo.seek(0) 

df = pd.read_csv(fo) 
print(df) 
+0

@brittenb, potresti testare anche la mia versione aggiornata rispetto ai tuoi dati? Grazie! Dovrebbe funzionare anche per le colonne con parentesi quadre, indipendentemente dalle loro posizioni ... – MaxU

+0

C'è un errore di battitura nel codice aggiornato? Non metterebbe 'fo = six.StringIO()' all'interno del ciclo 'for' per creare un nuovo file ogni volta? Sto solo chiedendo perché non ho mai usato il modulo 'six' prima, quindi forse è diverso. – brittenb

+0

@brittenb, grazie per la correzione! Ho aggiornato la mia risposta. Io uso 'six' solo per ragioni di compatibilità sp che questo codice funzionerà con entrambe le versioni di Python - Python 2 e Python 3. Quindi puoi usare io.StringIO. – MaxU