f# - FSharp.Data CsvProvider rendimiento
mono f#-data (2)
Tengo un archivo csv con 6 columnas y 678,552 filas. Lamentablemente, no puedo compartir ninguna muestra de datos pero los tipos son sencillos: int64
, int64
, date
, date
, string
, string
y no hay valores perdidos.
Tiempo para cargar estos datos en un marco de datos en R usando read.table
: ~ 3 segundos.
Es hora de cargar estos datos usando CsvFile.Load en F #: ~ 3 segundos.
Es hora de cargar estos datos en un marco de datos de Deedle en F #: ~ 7 segundos.
Agregar inferTypes=false
y proporcionar un esquema a Deedle''s Frame.ReadCsv
reduce el tiempo a ~ 3 segundos
Es hora de cargar estos datos usando CsvProvider en F #: ~ 5 minutos .
Y estos 5 minutos son incluso después de que defino los tipos en el parámetro Schema
, presumiblemente eliminando el tiempo que F # usaría para inferirlos.
Entiendo que el proveedor de tipos necesita hacer mucho más que R o CsvFile.Load para analizar los datos en el tipo de datos correcto, pero me sorprende la penalización de velocidad x100. Aún más confuso es el tiempo que toma Deedle para cargar los datos, ya que también necesita inferir tipos y emitir adecuadamente, organizar en Series, etc. De hecho, habría esperado que Deedle tardara más que CsvProvider.
En esta edición, el mal rendimiento de CsvProvider fue causado por un gran número de columnas, que no es mi caso.
Me pregunto si estoy haciendo algo mal o si hay alguna manera de acelerar un poco las cosas.
Solo para aclarar: crear el proveedor es casi instantáneo. Es cuando Seq.length df.Rows
que la secuencia generada se realice por Seq.length df.Rows
que tardan ~ 5 minutos para que vuelva el prompt de fsharpi.
Estoy en un sistema Linux, F # v4.1 en mono v4.6.1.
Aquí está el código para CsvProvider
let [<Literal>] SEP = "|"
let [<Literal>] CULTURE = "sv-SE"
let [<Literal>] DATAFILE = dataroot + "all_diagnoses.csv"
type DiagnosesProvider = CsvProvider<DATAFILE, Separators=SEP, Culture=CULTURE>
let diagnoses = DiagnosesProvider()
EDIT1: agregué el tiempo que toma Deedle para cargar los datos en un marco.
EDIT2: se agregó el tiempo que toma Deedle si inferTypes=false
y se proporciona un esquema.
Además, el suministro de CacheRows=false
en CsvProvider como se sugiere en los comentarios no tiene un efecto perceptible en el tiempo de carga.
EDIT3: Ok, estamos llegando a algún lado. Por alguna razón peculiar parece que la Culture
es la culpable. Si omito este argumento, CsvProvider carga los datos en ~ 7 segundos. No estoy seguro de qué podría estar causando esto. La configuración regional de mi sistema es en_US. Sin embargo, los datos provienen de un servidor SQL en la configuración sueca donde los dígitos decimales están separados por '','' en lugar de ''.''. Este conjunto de datos en particular no tiene decimales, por lo que puedo omitir Cultura por completo. Sin embargo, otro conjunto tiene 2 columnas decimales y más de 1,000,000 filas. Mi siguiente tarea es probar esto en un sistema Windows que no tengo disponible en este momento.
EDIT4: El problema parece resuelto, pero todavía no entiendo qué lo causa. Si cambio la cultura "globalmente" haciendo:
System.Globalization.CultureInfo.DefaultThreadCurrentCulture = CultureInfo("sv-SE")
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo("sv-SE")
y luego elimine el argumento Culture="sv-SE"
del CsvProvider, el tiempo de carga se reduce a ~ 6 segundos y los decimales se analizan correctamente. Lo dejo abierto en caso de que alguien pueda dar una explicación de este comportamiento.
El problema se debe a que CsvProvider no ha memorizado el Culture
explícitamente establecido. El problema fue abordado por esta solicitud de extracción.
Intento reproducir el problema que estás viendo, ya que no puedes compartir los datos. Intenté generar algunos datos de prueba. Sin embargo, en mi máquina (.NET 4.6.2 F # 4.1) no veo que tome unos minutos, lleva unos segundos.
Quizás pueda intentar ver cómo funciona mi aplicación de muestra en su configuración y podemos trabajar a partir de eso.
open System
open System.Diagnostics
open System.IO
let clock =
let sw = Stopwatch ()
sw.Start ()
fun () ->
sw.ElapsedMilliseconds
let time a =
let before = clock ()
let v = a ()
let after = clock ()
after - before, v
let generateDataSet () =
let random = Random 19740531
let firstDate = DateTime(1970, 1, 1)
let randomInt () = random.Next () |> int64 |> (+) 10000000000L |> string
let randomDate () = (firstDate + (random.Next () |> float |> TimeSpan.FromSeconds)).ToString("s")
let randomString () =
let inline valid ch =
match ch with
| ''"''
| ''//' -> '' ''
| _ -> ch
let c = random.Next () % 16
let g i =
if i = 0 || i = c + 1 then ''"''
else 32 + random.Next() % (127 - 32) |> char |> valid
Array.init (c + 2) g |> String
let columns =
[|
"Id" , randomInt
"ForeignId" , randomInt
"BirthDate" , randomDate
"OtherDate" , randomDate
"FirstName" , randomString
"LastName" , randomString
|]
use sw = new StreamWriter ("perf.csv")
let headers = columns |> Array.map fst |> String.concat ";"
sw.WriteLine headers
for i = 0 to 700000 do
let values = columns |> Array.map (fun (_, f) -> f ()) |> String.concat ";"
sw.WriteLine values
open FSharp.Data
[<Literal>]
let sample = """Id;ForeignId;BirthDate;OtherDate;FirstName;LastName
11795679844;10287417237;2028-09-14T20:33:17;1993-07-21T17:03:25;",xS@ %aY)N*})Z";"ZP~;"
11127366946;11466785219;2028-02-22T08:39:57;2026-01-24T05:07:53;"H-/QA(";"g8}J?k~"
"""
type PerfFile = CsvProvider<sample, ";">
let readDataWithTp () =
use streamReader = new StreamReader ("perf.csv")
let csvFile = PerfFile.Load streamReader
let length = csvFile.Rows |> Seq.length
printfn "%A" length
[<EntryPoint>]
let main argv =
Environment.CurrentDirectory <- AppDomain.CurrentDomain.BaseDirectory
printfn "Generating dataset..."
let ms, _ = time generateDataSet
printfn " took %d ms" ms
printfn "Reading dataset..."
let ms, _ = time readDataWithTp
printfn " took %d ms" ms
0
Los números de rendimiento (.NET462 en mi escritorio):
Generating dataset...
took 2162 ms
Reading dataset...
took 6156 ms
Los números de rendimiento (Mono 4.6.2 en mi Macbook Pro):
Generating dataset...
took 4432 ms
Reading dataset...
took 8304 ms
Actualizar
Resulta que especificar Culture
to CsvProvider explícitamente parece degradar el rendimiento mucho. Puede ser cualquier cultura, no solo sv-SE
pero ¿por qué?
Si uno verifica el código que el proveedor genera para los casos rápidos y lentos, notará una diferencia:
Rápido
internal sealed class csvFile@78
{
internal System.Tuple<long, long, System.DateTime, System.DateTime, string, string> Invoke(object arg1, string[] arg2)
{
Microsoft.FSharp.Core.FSharpOption<string> fSharpOption = TextConversions.AsString(arg2[0]);
long arg_C9_0 = TextRuntime.GetNonOptionalValue<long>("Id", TextRuntime.ConvertInteger64("", fSharpOption), fSharpOption);
fSharpOption = TextConversions.AsString(arg2[1]);
long arg_C9_1 = TextRuntime.GetNonOptionalValue<long>("ForeignId", TextRuntime.ConvertInteger64("", fSharpOption), fSharpOption);
fSharpOption = TextConversions.AsString(arg2[2]);
System.DateTime arg_C9_2 = TextRuntime.GetNonOptionalValue<System.DateTime>("BirthDate", TextRuntime.ConvertDateTime("", fSharpOption), fSharpOption);
fSharpOption = TextConversions.AsString(arg2[3]);
System.DateTime arg_C9_3 = TextRuntime.GetNonOptionalValue<System.DateTime>("OtherDate", TextRuntime.ConvertDateTime("", fSharpOption), fSharpOption);
fSharpOption = TextConversions.AsString(arg2[4]);
string arg_C9_4 = TextRuntime.GetNonOptionalValue<string>("FirstName", TextRuntime.ConvertString(fSharpOption), fSharpOption);
fSharpOption = TextConversions.AsString(arg2[5]);
return new System.Tuple<long, long, System.DateTime, System.DateTime, string, string>(arg_C9_0, arg_C9_1, arg_C9_2, arg_C9_3, arg_C9_4, TextRuntime.GetNonOptionalValue<string>("LastName", TextRuntime.ConvertString(fSharpOption), fSharpOption));
}
}
Lento
internal sealed class csvFile@78
{
internal System.Tuple<long, long, System.DateTime, System.DateTime, string, string> Invoke(object arg1, string[] arg2)
{
Microsoft.FSharp.Core.FSharpOption<string> fSharpOption = TextConversions.AsString(arg2[0]);
long arg_C9_0 = TextRuntime.GetNonOptionalValue<long>("Id", TextRuntime.ConvertInteger64("sv-SE", fSharpOption), fSharpOption);
fSharpOption = TextConversions.AsString(arg2[1]);
long arg_C9_1 = TextRuntime.GetNonOptionalValue<long>("ForeignId", TextRuntime.ConvertInteger64("sv-SE", fSharpOption), fSharpOption);
fSharpOption = TextConversions.AsString(arg2[2]);
System.DateTime arg_C9_2 = TextRuntime.GetNonOptionalValue<System.DateTime>("BirthDate", TextRuntime.ConvertDateTime("sv-SE", fSharpOption), fSharpOption);
fSharpOption = TextConversions.AsString(arg2[3]);
System.DateTime arg_C9_3 = TextRuntime.GetNonOptionalValue<System.DateTime>("OtherDate", TextRuntime.ConvertDateTime("sv-SE", fSharpOption), fSharpOption);
fSharpOption = TextConversions.AsString(arg2[4]);
string arg_C9_4 = TextRuntime.GetNonOptionalValue<string>("FirstName", TextRuntime.ConvertString(fSharpOption), fSharpOption);
fSharpOption = TextConversions.AsString(arg2[5]);
return new System.Tuple<long, long, System.DateTime, System.DateTime, string, string>(arg_C9_0, arg_C9_1, arg_C9_2, arg_C9_3, arg_C9_4, TextRuntime.GetNonOptionalValue<string>("LastName", TextRuntime.ConvertString(fSharpOption), fSharpOption));
}
}
Más específico esta es la diferencia:
// Fast
TextRuntime.ConvertDateTime("", fSharpOption), fSharpOption)
// Slow
TextRuntime.ConvertDateTime("sv-SE", fSharpOption), fSharpOption)
Cuando especificamos una cultura, esto se pasa a ConvertDateTime
que lo reenvía a GetCulture
static member GetCulture(cultureStr) =
if String.IsNullOrWhiteSpace cultureStr
then CultureInfo.InvariantCulture
else CultureInfo cultureStr
Esto significa que, para el caso predeterminado, usamos CultureInfo.InvariantCulture
pero para cualquier otro caso para cada campo y fila estamos creando un objeto CultureInfo
. El almacenamiento en caché se puede hacer, pero no es así. El proceso de creación en sí no parece tomar demasiado tiempo, pero sucede algo cuando estamos analizando con un nuevo objeto CultureInfo
cada vez.
El análisis de DateTime
en FSharp.Data
esencialmente es esto
let dateTimeStyles = DateTimeStyles.AllowWhiteSpaces ||| DateTimeStyles.RoundtripKind
match DateTime.TryParse(text, cultureInfo, dateTimeStyles) with
Así que hagamos una prueba de rendimiento en la que usemos un objeto CultureInfo
caché y otro en el que creamos uno cada vez.
open System
open System.Diagnostics
open System.Globalization
let clock =
let sw = Stopwatch ()
sw.Start ()
fun () ->
sw.ElapsedMilliseconds
let time a =
let before = clock ()
let v = a ()
let after = clock ()
after - before, v
let perfTest c cf () =
let dateTimeStyles = DateTimeStyles.AllowWhiteSpaces ||| DateTimeStyles.RoundtripKind
let text = DateTime.Now.ToString ("", cf ())
for i = 1 to c do
let culture = cf ()
DateTime.TryParse(text, culture, dateTimeStyles) |> ignore
[<EntryPoint>]
let main argv =
Environment.CurrentDirectory <- AppDomain.CurrentDomain.BaseDirectory
let ct = "sv-SE"
let cct = CultureInfo ct
let count = 10000
printfn "Using cached CultureInfo object..."
let ms, _ = time (perfTest count (fun () -> cct))
printfn " took %d ms" ms
printfn "Using fresh CultureInfo object..."
let ms, _ = time (perfTest count (fun () -> CultureInfo ct))
printfn " took %d ms" ms
0
Números de rendimiento en .NET 4.6.2 F # 4.1:
Using cached CultureInfo object...
took 16 ms
Using fresh CultureInfo object...
took 5328 ms
Por lo tanto, parece almacenar en caché el objeto CultureInfo
en FSharp.Data
debería mejorar significativamente el rendimiento de CsvProvider
cuando se especifica culture.