lunes 17 de octubre de 2011

Ignorar Tiempo en Query Linq to Entities

En Linq to Entities realizar una comparación de fechas sin tener en cuenta la hora me presentó un problema.

Mi código era:
var RessSor = (from s in db.sorteoLoteria
    join l in db.dic_loteria on s.fk_loteriaId equals l.pk_id
    where s.fecha.Date == dt.Date
    select new
    {
        NombreComercial = l.nombreComercial,
        NombreSEO = l.nombreSEO
    });
Este código funcionaba correctamente en LinQ to SQL, pero en LinQ to Entities no. Investigando un poco encontré la solución:
using System.Data.Objects;
...
var RessSor = (from s in db.sorteoLoteria
    join l in db.dic_loteria on s.fk_loteriaId equals l.pk_id
    where EntityFunctions.TruncateTime(s.fecha) == dt.Date
    select new
    {
        NombreComercial = l.nombreComercial,
        NombreSEO = l.nombreSEO
    });

miércoles 12 de octubre de 2011

Algoritmo Distancia Hamming Levenshtein en C#

El algoritmo Distancia de Hamming compara uno por uno los caracteres para ver cambios de uno con respecto al otro.

El algoritmo de Distancia de Levenshtein calcula numero de operaciones para convertir una cadena a otra.


Distancia de Hamming en C#
public static int HammingDistance(String s1, String s2)
        {
            int counter = 0;
            for (int k = 0; k < s1.Length; k++)
            {
                if (s1.ElementAt(k) == s2.ElementAt(k)) counter++;
            }
            return counter;
        }

Distancia de Levenshtein en C#
static int LevenshteinDistance(string s, string t, out double porcentaje)
        {
            porcentaje = 0;

            // d es una tabla con m+1 renglones y n+1 columnas
            int costo = 0;
            int m = s.Length;
            int n = t.Length;
            int[,] d = new int[m + 1, n + 1];

            // Verifica que exista algo que comparar
            if (n == 0) return m;
            if (m == 0) return n;

            // Llena la primera columna y la primera fila.
            for (int i = 0; i <= m; d[i, 0] = i++) ;
            for (int j = 0; j <= n; d[0, j] = j++) ;

            /// recorre la matriz llenando cada unos de los pesos.
            /// i columnas, j renglones
            for (int i = 1; i <= m; i++)
            {
                // recorre para j
                for (int j = 1; j <= n; j++)
                {
                    /// si son iguales en posiciones equidistantes el peso es 0
                    /// de lo contrario el peso suma a uno.
                    costo = (s[i - 1] == t[j - 1]) ? 0 : 1;
                    d[i, j] = System.Math.Min(System.Math.Min(d[i - 1, j] + 1,  //Eliminacion
                        d[i, j - 1] + 1),  //Inserccion 
                        d[i - 1, j - 1] + costo);  //Sustitucion
                }
            }
            /// Calculamos el porcentaje de cambios en la palabra.
            if (s.Length > t.Length)
                porcentaje = ((double)d[m, n] / (double)s.Length);
            else
                porcentaje = ((double)d[m, n] / (double)t.Length);
            return d[m, n];
        }

Reducción Dimensiones Usando Entropía

Para este algoritmo se utilizó el algoritmo de Distancia de Hamming y se realizaron todas las adecuaciones para ir buscando la mínima entropía eliminando columnas de datos hasta al final tener solamente 2 columnas. Tener en cuenta que como el desarrollo se realizo en c# las columnas inician en 0.

Para ayuda de vectores se usó la librería Math.Net Numerics
public static string Entropy(ArrayList arrText)
{
    string retorno = "";
    //Total de columnas
    string sT = arrText[0].ToString();
    double F = (double)Tools.ContarEjemplos(sT, ",");
    double E = 0;
    int i, j = 0;
    DenseMatrix S = new DenseMatrix(arrText.Count, arrText.Count, 0);

    for (i = 0; i < arrText.Count; i++)
    {
        for (j = i + 1; j < arrText.Count; j++)
        {
            S[i, j] = Tools.HammingDistance(arrText[i].ToString().Replace(",", ""), arrText[j].ToString().Replace(",", "")) / F;
        }
    }
    //A diferencia de la formula en c# la matrix inicia en 0, 0
    for (i = 0; i < arrText.Count - 1; i++)
    {
        for (j = i + 1; j < arrText.Count; j++)
        {
            if (S[i, j] > 0 && S[i, j] < 1)
            {
                E += (S[i, j] * Math.Log10(S[i, j]) + (1 - S[i, j]) * Math.Log10(1 - S[i, j]));
            }
        }
    }
    E = -1 * E;
    //Calcular las entropias eliminando una columna. Se guarda la minima.
    double MinE = E;
    double NewE = E;
    int indiceMinE = 0;
    double TempE = 0;
    int totalF = (int)F;
    for (int features = totalF; features >= 2; features--)
    {
        F = features;
        indiceMinE = 0;
        for (int f = 0; f < F; f++)
        {
            S.Clear();
            TempE = 0;

            for (i = 0; i < arrText.Count; i++)
            {
                for (j = i + 1; j < arrText.Count; j++)
                {
                    S[i, j] = Tools.HammingDistance(arrText[i].ToString().Replace(",", "").Remove(f, 1), arrText[j].ToString().Replace(",", "").Remove(f, 1)) / (F - 1);
                }
            }
            //A diferencia de la formula en c# la matrix inicia en 0, 0
            for (i = 0; i < arrText.Count - 1; i++)
            {
                for (j = i + 1; j < arrText.Count; j++)
                {
                    if (S[i, j] > 0 && S[i, j] < 1)
                    {
                        TempE += (S[i, j] * Math.Log10(S[i, j]) + (1 - S[i, j]) * Math.Log10(1 - S[i, j]));
                    }
                }
            }
            TempE = -1 * TempE;
            if (Math.Abs(E - TempE) < MinE)
            {
                MinE = Math.Abs(E - TempE);
                NewE = TempE;
                indiceMinE = f;
            }
        }
        //Se suma 1 al indice porque en c# los indices inician en 0
        retorno += "Columna con Entropia minima: " + (indiceMinE + 1).ToString() + "\n";
        //Eliminar la caracteristica del conjunto de datos
        for (i = 0; i < arrText.Count; i++)
        {
            arrText[i] = arrText[i].ToString().Replace(",", "").Remove(indiceMinE, 1);
        }
        //Asignar el nuevo E
        E = NewE;
        MinE = E;
    }
    return retorno;
}

Para la prueba de este algoritmo se usaron los siguientes datos como datos de prueba:

X,1,U,5,P,A
Y,2,D,7,O,B
Y,2,U,8,Q,C
X,1,U,7,O,B
Z,3,P,8,Q,C
Y,2,P,5,P,A
X,3,U,5,P,A

Resultados Obtenidos:
Columna con Entropía mínima: 4
Columna con Entropía mínima: 1
Columna con Entropía mínima: 3
Columna con Entropía mínima: 1
Columna con Entropía mínima: 1


Los resultados indican:

Iteración 1: La columna que minimiza la diferencia entre la entropía F y el valor correspondiente a la entropía sin esa columna es la 4. Eliminando esa columna del conjunto de datos obtenemos:

X,1,U,P,A
Y,2,D,O,B
Y,2,U,Q,C
X,1,U,O,B
Z,3,P,Q,C
Y,2,P,P,A
X,3,U,P,A

Iteración 2: La columna que minimiza la diferencia entre la entropía F y el valor correspondiente a la entropía sin esa columna es la 1. Eliminando esa columna del conjunto de datos obtenemos:

1,U,P,A
2,D,O,B
2,U,Q,C
1,U,O,B
3,P,Q,C
2,P,P,A
3,U,P,A

Iteración 3: La columna que minimiza la diferencia entre la entropía F y el valor correspondiente a la entropía sin esa columna es la 3. Eliminando esa columna del conjunto de datos obtenemos:

1,U,A
2,D,B
2,U,C
1,U,B
3,P,C
2,P,A
3,U,A

Iteración 4: La columna que minimiza la diferencia entre la entropía F y el valor correspondiente a la entropía sin esa columna es la 1. Eliminando esa columna del conjunto de datos obtenemos:

U,A
D,B
U,C
U,B
P,C
P,A
U,A

Iteración 5: La columna que minimiza la diferencia entre la entropía F y el valor correspondiente a la entropía sin esa columna es la 1. Eliminando esa columna del conjunto de datos obtenemos:

A
B
C
B
C
A
A

Al visualizar los resultados obtenidos podemos tener una visualización inicial de un ranking de importancia para cada una de las columnas o características de nuestro conjunto de datos. Esto por si deseamos posteriormente reducir la dimensión de este.

Minimización de Error en Discretización Binning

Esta implementación se realizó en C# con ayuda de la librería Math.Net Numerics para matrices y vectores.

Los pasos que se siguieron fueron los siguientes:

1. Organización de los datos de menor a mayor
2. Ejecución del algoritmo BIN Equal-Depth
3. Realizar medición del error inicial y realizar todos los posibles movimientos de ítems de izquierda a derecha para ver si se minimiza el error.
4. Realizar todos los posibles movimientos de derecha a izquierda para ver si se minimiza el error.
5. En caso de que el error luego de efectuar los movimientos sea inferior al error inicial se retorna al paso 3. Si ya el error no se minimiza más se finaliza. El error se mide en base a la suma de las desviaciones con respecto al promedio en cada uno de los grupos.
public static DenseVector[] MinimizeER(DenseVector[] vBins)
{
    DenseVector[] vOutput = new DenseVector[vBins.Length];
    int i = 0;
    int j = 0;

    double Error = CalcularTotalError(vBins);
    double ErrorInicial = Error;
    double ErrorFinal = 0;
    //While Global para volver a iterar si el error todavia se puede disminuir
    while (ErrorInicial > ErrorFinal)
    {
        ErrorInicial = Error;
        //Realizar ajustes en movimientos de items de Izquierda a Derecha
        for (j = 0; j < vBins.Length - 1; j++)
        {
            for (i = 0; i < vBins.Length; i++)
            {
                if (i == j)
                {
                    vOutput[i] = (DenseVector)vBins[i].SubVector(0, vBins[i].Count - 1);
                    //al siguiente vector se le agrega el ultimo item del anterior y la totalidad del siguiente
                    //Se convierte en array antes de iniciar
                    var list = new List();
                    list.AddRange(vBins[i].SubVector(vBins[i].Count - 1, 1).ToArray());
                    list.AddRange(vBins[i + 1].ToArray());
                    vOutput[i + 1] = new DenseVector(list.ToArray());
                    i++; //Esta suma es necesaria ya que si en el i+1 se nos iba por el else, se perdia lo realizado.
                }
                else
                {
                    vOutput[i] = vBins[i];
                }
            }
            //Evaluar el error. Si se redujo volver a realizar el proceso con el mismo j
            if (Error > CalcularTotalError(vOutput))
            {
                j = j - 1; //Se devuelve para que ensaye moviendo en el mismo par de bins
                Error = CalcularTotalError(vOutput);
                vBins = (DenseVector[])vOutput.Clone();
            }
        }

        //Realizar ajustes en movimientos de items de Derecha a Izquierda
        for (j = 1; j < vBins.Length; j++)
        {
            for (i = 0; i < vBins.Length; i++)
            {
                if (i == j)
                {
                    vOutput[i] = (DenseVector)vBins[i].SubVector(1, vBins[i].Count - 1);
                    //al siguiente vector se le agrega el ultimo item del anterior y la totalidad del siguiente
                    //Se convierte en array antes de iniciar
                    var list = new List();
                    list.AddRange(vBins[i - 1].ToArray());
                    list.AddRange(vBins[i].SubVector(0, 1).ToArray());
                    vOutput[i - 1] = new DenseVector(list.ToArray());
                }
                else
                {
                    vOutput[i] = vBins[i];
                }
            }
            //Evaluar el error. Si se redujo volver a realizar el proceso con el mismo j
            if (Error > CalcularTotalError(vOutput))
            {
                j = j - 1; //Se devuelve para que ensaye moviendo en el mismo par de bins
                Error = CalcularTotalError(vOutput);
                vBins = (DenseVector[])vOutput.Clone();
            }
        }
        ErrorFinal = Error;
    }
    return vOutput;
}

Iniciando con:
Bin 1: 1,1,2,2,3,3
Bin 2: 3,4,5,5,5
Bin 3: 5,7,8,9,10

Se Obtuvo:
Bin 1: 1,1,2,2,3,3,3,4
Bin 2: 5,5,5,5,7
Bin 3: 8,9,10

Respecto a lo obtenido en este último algoritmo, creo que teniendo en cuenta que se nos da la cantidad de Bins, se puede obtener una de las clasificaciones óptimas, o una aproximación muy cercana, teniendo en cuenta que minimizamos el error total.

Algoritmos de Discretización Binning

En Mineria de Datos una de las técnicas que podemos emplear para realizar la discretización de nuestros datos es la de separar nuestros datos en BINs (Binning discretization).

Esta discretización hace parte del pre-procesamieto inicial que debemos realizar a los datos.

El desarrollo de los algoritmos se realizó en .NET con el lenguaje c#. Para ayuda de vectores se usó la librería Math.Net Numerics.

Para los resultados aquí mostrados se usaron 3 Bins y se usaron los siguientes datos:
3,2,1,5,4,3,1,7,5,3,8,10,9,5,5,2

Para el algoritmo BIN se implementaron los siguientes modos:

Anchos Iguales (BIN Equal-Width)
Este algoritmo organiza los Bins en grupos donde el salto de cada uno es: (maximo-minimo)/N, donde N es el numero de bins que se desean.

public static DenseVector[] ObtenerBinsEqualWidth(double[] data, int nBins)
{
    int i = 0;

    double maximo = data.Max();
    double minimo = data.Min();

    //Se organizan los Bins con saltos iguales
    double salto = (maximo - minimo) / nBins;

    //Organizar de menor a mayor el arreglo
    var sortedlData = data.OrderBy(x => x);

    DenseVector[] vBins = new DenseVector[0];

    i = 0;
    double inicial = minimo;
    while (i < nBins)
    {
        //Redimensiona el arreglo para el nuevo BIN y luego asigna los items cuyo index se encuentre en el rango.
        Array.Resize(ref vBins, vBins.Length + 1);
        vBins[i] = new DenseVector(sortedlData.Where((x, index) => x >= inicial && x <= inicial + salto).ToArray());
        i++;
        inicial = i * salto + 1;
    }

    return vBins;
}

Resultados:
Bin 1: 1,1,2,2,3,3,3,4
Bin 2: 5,5,5,5,7,8
Bin 3: 9,10

Como el salto en este caso 10-1/3 es 3 el primer grupo tiene de 1 hasta 1+3=4, el segundo tiene desde 5 hasta 5+3=8 y el ultimo tiene los restantes.

Igualando Cantidad x Grupo (BIN Equal-Depth)
Este algoritmo distribuye uniformemente los datos dentro de los grupos tratando de que cada grupo quede con la misma cantidad de datos.

En caso de no lograr asignar la misma cantidad de datos a cada grupo, los primeros grupos quedan con un dato de más.
public static DenseVector[] ObtenerBinsEqualDepth(double[] data, int nBins)
{
    int i = 0;
    //Organizar de menor a mayor el arreglo
    var sortedlData = data.OrderBy(a => a);

    //BIN Equal-Depth
    int tam, res;
    DenseVector[] vBins = new DenseVector[0];
    //Realizar division Entera y Residuo para sacar tamaño de grupos
    tam = Math.DivRem(data.Length, nBins, out res);
    i = 0;
    int indexGroup = 0;
    if (res > 0)
    {
        while (res > 0)
        {
            //Redimensiona el arreglo para el nuevo BIN y luego asigna los items cuyo index se encuentre en el rango. Se suma uno al tamaño por el residuo
            Array.Resize(ref vBins, vBins.Length + 1);
            vBins[i] = new DenseVector(sortedlData.Where((f, index) => index >= indexGroup && index < indexGroup + tam + 1).ToArray());
            indexGroup = indexGroup + tam + 1;
            i++;
            res--;
        }
    }
    while (i < nBins)
    {
        //Redimensiona el arreglo para el nuevo BIN y luego asigna los items cuyo index se encuentre en el rango.
        Array.Resize(ref vBins, vBins.Length + 1);
        vBins[i] = new DenseVector(sortedlData.Where((f, index) => index >= indexGroup && index < indexGroup + tam).ToArray());
        indexGroup = indexGroup + tam;
        i++;
    }

    return vBins;
}

Resultados:
Bin 1: 1,1,2,2,3,3
Bin 2: 3,4,5,5,5
Bin 3: 5,7,8,9,10

En este caso se trataron de distribuir la misma cantidad de números en cada bin. Como en total eran 16 datos se distribuyo 6 en el primer grupo y 5 en los dos siguientes.


Grupos con Suavizado (BIN with Smoothing)
Para este algoritmo se dejaron las opciones de valor medio y promedio de los datos.

En los dos casos se ejecuta primero el BIN Equal-Depth y sobre estos resultados se realiza el remplazo en cada bin, ya sea por el valor medio o por el promedio.
public static DenseVector[] ObtenerBinsStatistic(DenseVector[] vBins, int type)
{
    DenseVector[] vOutput = new DenseVector[vBins.Length];
    int i = 0;
    foreach (DenseVector bin in vBins)
    {
        var statistics = new DescriptiveStatistics(bin);
        if (type == 1)
        {
            vOutput[i] = new DenseVector(bin.Count, statistics.Median);
        }
        else
        {
            vOutput[i] = new DenseVector(bin.Count, statistics.Mean);
        }
        i++;
    }
    return vOutput;
}

Resultados con el valor medio:
Bin 1: 2,2,2,2,2,2
Bin 2: 5,5,5,5,5
Bin 3: 8,8,8,8,8

Si nos fijamos en estos bins el valor corresponde justo al valor medio de los resultados obtenidos con el algoritmo de BIN Equal-Depth.

Resultados con el promedio:
Bin 1: 2,2,2,2,2,2
Bin 2: 4.4,4.4,4.4,4.4,4.4
Bin 3: 7.8,7.8,7.8,7.8,7.8

Estos resultados corresponden al valor promedio  de los resultados obtenidos con el algoritmo de BIN Equal-Depth.

domingo 20 de febrero de 2011

Publicación en Twitter con ASP.NET

En un post anterior habia descrito la forma de publicar post mediante programación, pero Twitter cambio la forma de hacerlo. Ahora vamos a hacerlo mediante la libreria TwitterVB.

Primero debemos descargar la dll llamada TwitterVB.dll desde TwitterVB y agregarla a nuestro proyecto en la carpeta bin.

Luego accedemos a Twitter y luego al link https://dev.twitter.com/apps para registrar nuestra nueva aplicación que se actualizará desde código. Una vez registremos la aplicación vamos a obtener dos datos que vamos a necesitar. El primero es el Customer key y el segundo es el Customer Secret.


Ya desde programación realizamos lo siguiente:
TwitterVB2.TwitterAPI tw = new TwitterVB2.TwitterAPI();
string url = tw.GetAuthorizationLink(sCustomerKey, sCustomerSecret);

La url que obtenemos desde programación la invocamos en nuestro navegador... esto nos mostrará una ventana de validación de permisos de Twitter. Hacemos clic en permitir acceso y esto nos mostrará un PIN de validación:


El numero de validación nos servirá para obtener el Token y el TokenSecret. Estos los necesitamos guardar ya que son los que nos permitiran publicar Twitts. Actualmente estos datos son persistentes por lo cual podemos almacenarlos, pero si Twitter llegara a cambiar la autenticación probablemente deberiamos actualizarlos. Yo llevo varios meses funcionando asi y todavia no han cambiado.

Para obtener el Token y el TokenSecret se debe ejecutar:
bool isValidPIN = tw.ValidatePIN("4444444");

if (isValidPIN) {
  string OAuthToken = tw.OAuth_Token();
  string OAuthTokenSecret = tw.OAuth_TokenSecret();
}
Una vez ya tenga el token y el tokenSecret puede publicar los mensajes por medio de:
tw.AuthenticateWith(ConsumerKey, ConsumerKeySecret, OAuthToken, OAuthTokenSecret);
tw.Update("Mi primer mensaje");
Y ya... Como comentaba tanto el Token como el TokenSecret hasta el momento son persistentes por lo cual podria guardarlos en un archivo de configuración, base de datos o llamarlos directamente.

domingo 9 de enero de 2011

Guardar comentarios con api de facebook

La funcionalidad de guardar comentarios de uno de mis portales se desactivo debido a actualizaciones en la api de facebook.

Aunque la funcionalidad de escribir los comentarios se mantuvo intacta, la de guardarlos en mi base de datos se perdió.

El código que tenia era algo como lo siguiente:

<script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php" type="text/javascript"></script>
<div id="divfb_connect"></div>

<script type="text/javascript">
    var fbcDiv = document.getElementById('divfb_connect');
    var fbcUrl = 'http://www.mysite.com' + location.pathname;
    var fbcPath = location.pathname;
    var fbcTitle = document.title;
    var fbcXid = 'asdfqwer';

    fbcDiv.innerHTML = '<fb:comments numposts="10" candelete="true" canpost="true" url="' + fbcUrl + '" title="' + fbcTitle + '" xid="' + fbcXid + '" quiet="true"></fb:comments>';
    FB.init('21asdf231asd23f1', "xd_receiver.html");

    FB_RequireFeatures(["Comments"], function() {
    FB.CommentClient.add_onComment(function(comment) {

    var url = 'mypage.aspx?id=' + 279763 + '&comments=' + comment.post + '&user=' + comment.user;
    $.get(url, "", function handle_comments(data, status) {
        //code here
    });
    });

    });   
</script>

Investigando encontré que habia que realizar un cambio para usar la nueva versión. En esta se debia usar:

<div id="fb-root"></div>
<script>
  window.fbAsyncInit = function() {
    FB.init({appId: 'your app id', status: true, cookie: true,
             xfbml: true});
  };
  (function() {
    var e = document.createElement('script'); e.async = true;
    e.src = document.location.protocol +
      '//connect.facebook.net/en_US/all.js';
    document.getElementById('fb-root').appendChild(e);
  }());
</script>

Y para poder controlar el evento de cuando se agrega un comentario se usa la función:

FB.Event.subscribe('comments.add', function(response) {
        alert('Comment Added')
});


La cual debe ir dentro de la función de window.fbAsyncInit despues de la función init, y dentro del fb:comment hay que agregar la propiedad notify en true.

Pero ahí fue donde iniciaron los problemas. El objeto response no posee propiedades que permitan acceder ni al id del usuario ni al texto que escribio como comentario. Pero he aqui una solución ingeniosa... la idea es obtener el comentario que se acabo de escribir. El código queda:

<div id="aclfb_connect"></div>
<script type="text/javascript">
    var fbcDiv = document.getElementById('aclfb_connect');
    var fbcUrl = 'http://www.mysite.com' + location.pathname;
    var fbcPath = location.pathname;
    var fbcTitle = document.title;
    var fbcXid = 'asdfqwer';

    fbcDiv.innerHTML = '<fb:comments numposts="10" candelete="true" canpost="true" url="' + fbcUrl + '" title="' + fbcTitle + '" xid="' + fbcXid + '" quiet="true" notify="true"></fb:comments><div id="fb-root"></div>';

    window.fbAsyncInit = function () {
        FB.init({ appId: '21asdf231asd23f1', status: true, cookie: true,
            xfbml: true
        });
        FB.Event.subscribe('comments.add', function (resp) {
          FB.api({
              method: 'comments.get',
              xid: fbcXid
          },
          function (response) {                   
              var fb_commentData = {
                  xid: response[0].xid,
                  fromid: response[0].fromid,
                  time: response[0].time,
                  text: response[0].text,
                  id: response[0].id,
                  username: response[0].username,
                  reply_xid: response[0].reply_xid,
                  post_id: response[0].post_id,
                  app_id: response[0].app_id,
                  object_id: response[0].object_id
              };
              var url = 'mypage.aspx?id='+ response[0].xid
 +'&comments=' + response[0].text + '&user=' + response[0].fromid + '&id=' + response[0].id + '&username=' + response[0].username + '&reply_xid=' + response[0].reply_xid + '&post_id=' + response[0].post_id;
              $.get(url, "", function handle_comments(data, status) {
                  //code here
              });

          });
        });
    };
    (function () {
        var e = document.createElement('script'); e.async = true;
        e.src = document.location.protocol +
      '//connect.facebook.net/en_US/all.js';
        document.getElementById('fb-root').appendChild(e);
    } ());
  
</script>

Y por fin! logré guardar los comentarios que los usuarios escribian en mi portal.