F# and the Open/Closed Principle

One of the advantages of using F# is that it is a .NET language.  Although F# is a functional-first language, it also supports object-oriented constructs.  One of the most powerful (indeed, the most powerful) technique in OO programming is using interfaces to follow the Open/Closed principle.  If you are not familiar, a good explanation of Open/Closed principle is found here.

As part of the F# for beginners dojo I am putting on next week, we are consuming and then analyzing Twitter.  The problem with always making calls to Twitter is that

1) The data changes every call

2) You might get throttled

Therefore, it makes good sense to have an in-memory representation of the data for testing and some Twitter data on disk so that different experiments can be run on the same data to see the result.  Using Interfaces in F# makes this a snap.

First, I created an interface:

  1. namespace NewCo.TwitterAnalysis
  2.  
  3. open System
  4. open System.Collections.Generic
  5.  
  6. type ITweeetProvider =
  7.    abstract member GetTweets : string -> IEnumerable<DateTime * int * string>

Next, I created the actual Twitter feed.  Note I am using TweetInvi (available on Nuget) and that this file has to be below the interface in the solution explorer:

  1. namespace NewCo.TwitterAnalysis
  2.  
  3. open System
  4. open System.Configuration
  5. open Tweetinvi
  6.  
  7. type TwitterProvider() =
  8.     interface ITweeetProvider with
  9.         member this.GetTweets(stockSymbol: string) =
  10.             let consumerKey = ConfigurationManager.AppSettings.["consumerKey"]
  11.             let consumerSecret = ConfigurationManager.AppSettings.["consumerSecret"]
  12.             let accessToken = ConfigurationManager.AppSettings.["accessToken"]
  13.             let accessTokenSecret = ConfigurationManager.AppSettings.["accessTokenSecret"]
  14.         
  15.             TwitterCredentials.SetCredentials(accessToken, accessTokenSecret, consumerKey, consumerSecret)
  16.             let tweets = Search.SearchTweets(stockSymbol);
  17.             tweets
  18.                 |> Seq.map(fun t -> t.CreatedAt, t.RetweetCount, t.Text)

 

I then hooked up a unit (integration, really) test

  1. [TestClass]
  2. public class UnitTest1
  3. {
  4.     [TestMethod]
  5.     public void GetTweetsUsingIBM_returnsExpectedValue()
  6.     {
  7.         ITweeetProvider provider = new TwitterProvider();
  8.         var actual = provider.GetTweets("IBM");
  9.         Assert.IsNotNull(actual);
  10.     }
  11. }

Sure enough, it ran green with actual Twitter data coming back:

image

I then created an In-Memory Tweet provider that can be used to:

1) Provide repeatable results

2) Have 0 external dependencies so that I can monkey with the code and a red unit test really does mean red

Here is its implementation:

  1. namespace NewCo.TwitterAnalysis
  2.  
  3. open System
  4. open System.Collections.Generic
  5.  
  6. type InMemoryProvider() =
  7.     interface ITweeetProvider with
  8.         member this.GetTweets(stockSymbol: string) =
  9.             let list = new List<(DateTime*int*string)>()
  10.             list.Add(DateTime.Now, 1,"Test1")
  11.             list.Add(DateTime.Now, 0,"Test2")
  12.             list :> IEnumerable<(DateTime*int*string)>

The only really interesting thing is the smiley/bird character (: >).  F# implements interfaces a bit differently than what I was used to –> F# implements interfaces explicitly.  I then fired up a true unit test and it also ran green:

  1. [TestClass]
  2. public class InMemoryProviderTests
  3. {
  4.     [TestMethod]
  5.     public void GetTweetsUsingValidInput_ReturnsExpectedValue()
  6.     {
  7.         ITweeetProvider provider = new InMemoryProvider();
  8.         var tweets = provider.GetTweets("TEST");
  9.         var tweetList = tweets.ToList();
  10.         Int32 expected = 2;
  11.         Int32 actual = tweetList.Count;
  12.         Assert.AreEqual(expected, actual);
  13.     }
  14. }

Finally, I created a file-system bound provider so that I can download and then hold static a large dataset.  Based on past experience dealing with on-line data sources, getting data local to run multiple tests against is generally a good idea.  Here is the implementation:

  1. namespace NewCo.TwitterAnalysis
  2.  
  3. open System
  4. open System.Collections.Generic
  5. open System.IO
  6.  
  7. type FileSystemProvider(filePath: string) =
  8.     interface ITweeetProvider with
  9.         member this.GetTweets(stockSymbol: string) =
  10.             let fileContents = File.ReadLines(filePath)
  11.                                 |> Seq.map(fun line -> line.Split([|'\t'|]))
  12.                                 |> Seq.map(fun values -> DateTime.Parse(values.[0]),int values.[1], string values.[2])
  13.             fileContents

And the covering unit (integration really) tests look like this:

  1. [TestClass]
  2. public class FileSystemProviderTests
  3. {
  4.     [TestMethod]
  5.     public void GetTweetsUsingValidInput_ReturnsExpectedValue()
  6.     {
  7.         var baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
  8.         var testFile = Path.Combine(baseDir, "TweetData.csv");
  9.         ITweeetProvider provider = new FileSystemProvider(testFile);
  10.         var tweets = provider.GetTweets("TEST");
  11.         var tweetList = tweets.ToList();
  12.         Int32 expected = 2;
  13.         Int32 actual = tweetList.Count;
  14.         Assert.AreEqual(expected, actual);
  15.     }
  16. }

Note that I had to add the actual file in the test project. 

image

Finally, the F# code needs to include try..catches for the external calls (web service and disk) and some argument validation for the strings come in.

In any event, I now have 3 different implementations that I can swap out depending on my needs.  I love having the power of Interfaces combined with benefits of using a functional-first language.

Consuming Twitter With F#

I set up a meetup for TRINUG’s F#/data analytics SIG to center around consuming and analyzing Tweets.  Since Twitter is just JSON, I assumed it would be easy enough to search Tweets for a given subjects in a given time period.  How wrong I was.  I spent several hours research different ways to consume Twitter to varying degrees of success.  My 1st stop was to investigate some of the more common libraries that C# developers use to consume Twitter.  Here is my survey of some of the more popular ones:

Twitterizer: No longer maintained

  1. // Install-Package twitterizer -Version 2.4.2
  2. // Update-Package Newtonsoft.Json -Reinstall
  3. open Twitterizer
  4.  
  5. type public TwitterProvider() =
  6.     member this.GetTweetsForDateRange(ticker:string, startDate: DateTime, endDate: DateTime) =
  7.         let consumerKey = ConfigurationManager.AppSettings.["consumerKey"]
  8.         let consumerSecret = ConfigurationManager.AppSettings.["consumerSecret"]
  9.         let accessToken = ConfigurationManager.AppSettings.["accessToken"]
  10.         let accessTokenSecret = ConfigurationManager.AppSettings.["accessTokenSecret"]
  11.         
  12.         let tokens = new OAuthTokens()
  13.         tokens.set_ConsumerKey(consumerKey)
  14.         tokens.set_ConsumerSecret(consumerSecret)
  15.         tokens.set_AccessToken(accessToken)
  16.         tokens.set_AccessTokenSecret(accessTokenSecret)
  17.  
  18.         let searchOptions = new SearchOptions()
  19.         searchOptions.SinceDate <- startDate
  20.         searchOptions.UntilDate <- endDate
  21.         let results = TwitterSearch.Search(tokens, ticker,searchOptions)
  22.         results.ResponseObject
  23.                     |> Seq.map(fun r -> r.CreatedDate, r.Text)

TweetSharp: No longer maintained

  1. open TweetSharp
  2.  
  3. type public TwitterProvider() =
  4.     member this.GetTweetsForDateRange(ticker:string, startDate: DateTime, endDate: DateTime) =
  5.         let consumerKey = ConfigurationManager.AppSettings.["consumerKey"]
  6.         let consumerSecret = ConfigurationManager.AppSettings.["consumerSecret"]
  7.         let accessToken = ConfigurationManager.AppSettings.["accessToken"]
  8.         let accessTokenSecret = ConfigurationManager.AppSettings.["accessTokenSecret"]
  9.         
  10.         let service = new TwitterService(consumerKey, consumerSecret)
  11.         service.AuthenticateWith(accessToken, accessTokenSecret)
  12.  
  13.         let searchOptions = new SearchOptions()
  14.         searchOptions.Q <- "IBM%20since%3A2014-03-01&src=typd"
  15.         service.Search(searchOptions).Statuses
  16.                                         |> Seq.map(fun s -> s.CreatedDate, s.Text)

Note that I did try and add a date range the way the Twitter API instructs, but it still came back with only 20 tweets.

LinqToTwitter: Active but nave to use Linq syntax.  Ugh!

Twitterinvi: Active but does not have date range functionality

  1. open System
  2. open System.Configuration
  3. open Tweetinvi
  4.  
  5. type public TwitterProvider() =
  6.     member this.GetTodaysTweets(ticker: string) =
  7.         let consumerKey = ConfigurationManager.AppSettings.["consumerKey"]
  8.         let consumerSecret = ConfigurationManager.AppSettings.["consumerSecret"]
  9.         let accessToken = ConfigurationManager.AppSettings.["accessToken"]
  10.         let accessTokenSecret = ConfigurationManager.AppSettings.["accessTokenSecret"]
  11.  
  12.         TwitterCredentials.SetCredentials(accessToken, accessTokenSecret, consumerKey, consumerSecret)
  13.         let tweets = Search.SearchTweets(ticker);
  14.         tweets |> Seq.map(fun t -> t.CreatedAt, t.RetweetCount)
  15.  
  16.     member this.GetTweetsForDateRange(ticker: string, startDate: DateTime)=
  17.         let consumerKey = ConfigurationManager.AppSettings.["consumerKey"]
  18.         let consumerSecret = ConfigurationManager.AppSettings.["consumerSecret"]
  19.         let accessToken = ConfigurationManager.AppSettings.["accessToken"]
  20.         let accessTokenSecret = ConfigurationManager.AppSettings.["accessTokenSecret"]
  21.  
  22.         TwitterCredentials.SetCredentials(accessToken, accessTokenSecret, consumerKey, consumerSecret)
  23.         let searchParameter = Search.GenerateSearchTweetParameter(ticker)
  24.         searchParameter.Until <- startDate;
  25.         let tweets = Search.SearchTweets(searchParameter);
  26.         tweets |> Seq.map(fun t -> t.CreatedAt, t.RetweetCount)

So without an out of the box API to use, I thought about using a Json Type Provider the way Lincoln Atkinson did.  The problem is that is example is for V1 of Twitter and V 1.1 uses Oauth.  If you run his code, you get

image

I then thought about a 3rd party API that captures Tweets.  I ran across gnip ($500!) and Topsy (no longer accepting new licenses b/c Apple bought them) so I am back to square one.

So finally I thought about rolling my own (with OAuth being the hard part) but I am quickly running out of time to get ready for the SIG and I don’t want to spend the time on only this part. 

Why isn’t there a Twitter type provider?  I’ll add it to the list….

JavaScript Signature Capture Panel

I am attempting to teach myself some more JavaScript.  To that end I decided to replicate some of the projects I did in WPF/C# in HTML5/JavaScript.   One of the 1st ‘hello world’ projects I did in WPF was creating a signature panel – so it seemed like a good place to start.  The original blog post is here.   The original WPF project took advantage of the InkCanvas class.  Below is a code snippet of the how the events were captured in the original project:

  1. private void inkSignature_MouseDown(object sender, MouseButtonEventArgs e)
  2. {
  3.     IsCapturing = true;
  4.     glyph = new Glyph();
  5.  
  6. }
  7.  
  8. private void inkSignature_MouseUp(object sender, MouseButtonEventArgs e)
  9. {
  10.     IsCapturing = false;
  11.     _signature.Glyphs.Add(glyph);
  12.     startPoint = new Point();
  13.     endPoint = new Point();
  14.  
  15. }
  16.  
  17. private void inkSignature_MouseMove(object sender, MouseEventArgs e)
  18. {
  19.     if (IsCapturing)
  20.     {
  21.         if (startPoint.X == 0 && startPoint.Y == 0 && endPoint.X == 0 && endPoint.Y == 0)
  22.         {
  23.             endPoint = new Point(e.GetPosition(this).X, e.GetPosition(this).Y);
  24.         }
  25.         else
  26.         {
  27.             startPoint = endPoint;
  28.             endPoint = new Point(e.GetPosition(this).X, e.GetPosition(this).Y);
  29.             Line line = new Line(startPoint, endPoint);
  30.             glyph.Lines.Add(line);
  31.         }
  32.  
  33.     }
  34.  
  35. }

To have the same effect in the browser, I swapped out the InkCanvas with the Canvas tag.

  1. <canvas id="myCanvas" width="578" height="200" style="border:solid"></canvas>
  2. <br />
  3. <button id="resultButton" onclick="showSignature()"></button>

I then stubbed out the ‘mousedown’, ‘mouseup’, and ‘mousemove’ events to see if I was hooked up to them correctly and they were firing as expected:

  1. <body>
  2.  
  3.     <script>
  4.         canvas.addEventListener('mousemove', function (event) {
  5.         }, false);
  6.  
  7.         canvas.addEventListener('mousedown', function (event) {
  8.             alert("mousedown");
  9.         }, false);
  10.  
  11.         canvas.addEventListener('mouseup', function (event) {
  12.             alert("mouseup");
  13.         }, false);
  14.     </script>
  15.  
  16. </body>

I then thought about how to implement the InkCanvas code in JavaScript so I added some variables that all of the examples of StackOverflow use:

  1. var canvas = document.getElementById('myCanvas');
  2. var context = canvas.getContext('2d');

I then needed the function to calculate the mouse position relative to the signature panel (versus the screen).  This was also pretty common on StackOverflow:

  1. function getMousePosition(canvas, event) {
  2.     var rectangle = canvas.getBoundingClientRect();
  3.     return {
  4.         x: event.clientX – rectangle.left,
  5.         y: event.clientY – rectangle.top
  6.     };
  7. };

Finally, I could implement the WPF-equivalent logic.  First was the variables to maintain state:

  1. var isCapturing = false;
  2. var startX = 0;
  3. var startY = 0;
  4. var endX = 0;
  5. var endY = 0;
  6. var signature = [];
  7. var glyph = [];

And then the 3 event handlers:

  1. canvas.addEventListener('mousemove', function (event) {
  2.     if (isCapturing) {
  3.         var mousePosition = getMousePosition(canvas, event);
  4.  
  5.         if (startX === 0 && startY === 0 && endX === 0 && endY === 0) {
  6.             endX = mousePosition.x;
  7.             endY = mousePosition.y;
  8.         }
  9.         else {
  10.             startX = endX;
  11.             startY = endY;
  12.             endX = mousePosition.x;
  13.             endY = mousePosition.y;
  14.  
  15.             context.beginPath();
  16.             context.moveTo(startX, startY);
  17.             context.lineTo(endX, endY);
  18.             context.stroke()
  19.  
  20.             glyph.push(startX, startY, endX, endY);
  21.         }
  22.     }
  23. }, false);
  24.  
  25. canvas.addEventListener('mousedown', function (event) {
  26.     isCapturing = true;
  27.     glyph = [];
  28. }, false);
  29.  
  30. canvas.addEventListener('mouseup', function (event) {
  31.     isCapturing = false;
  32.     signature.push(glyph);
  33.     var startX = 0;
  34.     var startY = 0;
  35.     var endX = 0;
  36.     var endY = 0;
  37. }, false);

When I ran it, I <almost> got it right:

image

The problem is that the mouseup event was not resetting the starting value of the next point to 0, so the signature was coming out as 1 long line.  After sleeping on it (my pattern is write bugs at night, fix them in the morning), I realized I just had to reset the start and end coordinates on mouseup and then inspect in the mousemove.  Here is the complete final code:

  1. <!DOCTYPE html>
  2. <html xmlns="http://www.w3.org/1999/xhtml"&gt;
  3. <head>
  4.     <title></title>
  5. </head>
  6. <body>
  7.     <canvas id="myCanvas" width="578" height="200" style="border:solid"></canvas>
  8.     <br />
  9.     <button id="resultButton" onclick="showSignature()"></button>
  10.  
  11.  
  12.     <script>
  13.         function showSignature() {
  14.             alert(signature.length);
  15.         };
  16.     </script>
  17.  
  18.     <script>
  19.         var canvas = document.getElementById('myCanvas');
  20.         var context = canvas.getContext('2d');
  21.         var isCapturing = false;
  22.         var startX = 0;
  23.         var startY = 0;
  24.         var endX = 0;
  25.         var endY = 0;
  26.         var signature = [];
  27.         var glyph = [];
  28.  
  29.         function getMousePosition(canvas, event) {
  30.             var rectangle = canvas.getBoundingClientRect();
  31.             return {
  32.                 x: event.clientX – rectangle.left,
  33.                 y: event.clientY – rectangle.top
  34.             };
  35.         };
  36.  
  37.         canvas.addEventListener('mousemove', function (event) {
  38.             if (isCapturing) {
  39.                 var mousePosition = getMousePosition(canvas, event);
  40.  
  41.                 if (endX === 0 && endY === 0) {
  42.                     endX = mousePosition.x;
  43.                     endY = mousePosition.y;
  44.                 }
  45.                 else {
  46.                     startX = endX;
  47.                     startY = endY;
  48.                     endX = mousePosition.x;
  49.                     endY = mousePosition.y;
  50.  
  51.                     context.beginPath();
  52.                     context.moveTo(startX, startY);
  53.                     context.lineTo(endX, endY);
  54.                     context.stroke()
  55.  
  56.                     glyph.push(startX, startY, endX, endY);
  57.                 }
  58.             }
  59.         }, false);
  60.  
  61.         canvas.addEventListener('mousedown', function (event) {
  62.             isCapturing = true;
  63.             glyph = [];
  64.  
  65.             var mousePosition = getMousePosition(canvas, event);
  66.             var startX = mousePosition.x;
  67.             var startY = mousePosition.y;
  68.         }, false);
  69.  
  70.         canvas.addEventListener('mouseup', function (event) {
  71.             isCapturing = false;
  72.             signature.push(glyph);
  73.  
  74.             startX = 0;
  75.             startY = 0;
  76.             endX = 0;
  77.             endY = 0;
  78.         }, false);
  79.     </script>
  80.  
  81. </body>
  82. </html>

And here it is in action:

image

Now all I have to do is put the points into the same data structures that I used in the WPF project: Signature –> Glyphs[] –> Lines[] –> Line.StartPoint && Line.EndPoint.

 

 

Apriori Algorithm and F# Using Elevator Inspection Data

Now that I have the elevator dataset in a workable state, I wanted to see what I could see with the data.  I was reading Machine Learning In Action and the authors suggested that an Apriori Algorithm as a way to quantify associations among data points.  I read both Harrington’s code and Wikipedia’s description and I found both the be impenetrable – the former because their code was unreadable and the later because  the mathematical formulas depended on a level of algebra that I don’t have.

Fortunately, I found a C# project on Codeproject that had both an excellent example/introduction and C# code.  I used the examples on the website to formulate my F# implementation.

The first thing I did was create a class that matched the 1st grid in the example

image

  1. namespace ChickenSoftware.ElevatorChicken.Analysis
  2.  
  3. open System.Collections.Generic
  4.  
  5. type Transaction = {TID: string; Items: List<string> }
  6.  
  7. type Apriori(database: List<Transaction>, support: float, confidence: float) =
  8.     member this.Database = database
  9.     member this.Support = support
  10.     member this.Confidence = confidence

Note that because F# is immutable by default, the properties are read-only.  I then created a unit test project that makes sure the constructor works without exceptions.  The data matches the example:

  1. public AprioriTests()
  2. {
  3.     var database = new List<Transaction>();
  4.     database.Add(new Transaction("100", new List<string>() { "A", "C", "D" }));
  5.     database.Add(new Transaction("200", new List<string>() { "B", "C", "E" }));
  6.     database.Add(new Transaction("300", new List<string>() { "A", "B", "C", "E" }));
  7.     database.Add(new Transaction("400", new List<string>() { "B", "E" }));
  8.  
  9.     _apriori = new Apriori(database, .5, .80);
  10.  
  11. }
  12.  
  13. [TestMethod]
  14. public void ConstructorUsingValidArguments_ReturnsExpected()
  15. {
  16.     Assert.IsNotNull(_apriori);
  17. }

I then need a function to count up all of the items in the Itemsets.  I refused to use loops, so I first started using Seq.Fold, but I was having zero luck because I was trying to fold a Seq of List.  I then started experimenting with other functions when I found Seq.Collect – which was perfect.  So I created a function like this:

  1. member this.GetC1() =
  2.     database
  3.  
  4. member this.GetL1() =
  5.     let numberOfTransactions = this.GetC1().Count
  6.  
  7.     this.GetC1()
  8.         |> Seq.collect(fun d -> d.Items)
  9.         |> Seq.countBy(fun i -> i)
  10.         |> Seq.map(fun (t,i) -> t, i, float i/ float numberOfTransactions)
  11.         |> Seq.filter(fun (t,i,p) -> p >= support)
  12.         |> Seq.map(fun (t,i,p) -> t,i)
  13.         |> Seq.sort
  14.         |> Seq.toList

Note that the numberOfTransactions is for the database, not the individual items in the List<Item>.  And the results match the example:

imageimage

So this is great.  My next stop was to build a list of pair combinations of the remaining values

image

The trick is that is not a Cartesian join of the original sets – it is only the surviving sets that are needed.  My first attempt looked like:

  1. let C1 = database
  2.  
  3. let L1 = C1
  4.         |> Seq.map(fun t -> t.Items)
  5.         |> Seq.collect(fun i -> i)
  6.         |> Seq.countBy(fun i -> i)
  7.         |> Seq.map(fun (t,i) -> t, i, float i/ float numberOftransactions)
  8.         |> Seq.filter(fun (t,i,p) -> p >= support)
  9.         |> Seq.toArray
  10. let C2A = L1
  11.             |> Seq.map(fun (x,y,z) -> x)
  12.             |> Seq.toArray
  13. let C2B = L1
  14.             |> Seq.map(fun (x,y,z) -> x)
  15.             |> Seq.toArray
  16. let C2 = C2A |> Seq.collect(fun x -> C2B |> Seq.map(fun y -> x+y))
  17. C2   

With the output like this:

image

I was running out of Saturday morning so I went over to stack overflow and got a couple of responses.  I was on the right track with the concat, but I didn’t think about the List.Filter(), which would prune my list.  With this in mind, I copied Mark’s code and got what I was looking for

  1. member this.GetC2() =
  2.     let l1Itemset = this.GetL1()
  3.                     |> Seq.map(fun (i,s) -> i)
  4.  
  5.     let itemset =
  6.         l1Itemset
  7.             |> Seq.map(fun x -> l1Itemset |> Seq.map(fun y -> (x,y)))
  8.             |> Seq.concat
  9.             |> Seq.filter(fun (x,y) -> x < y)
  10.             |> Seq.sort
  11.             |> Seq.toList         
  12.     
  13.     let listContainsItem(l:List<string>, a,b) =
  14.             l.Contains(a) && l.Contains(b)
  15.     
  16.     let someFunctionINeedToRename(l1:List<string>, l2)=
  17.             l2 |> Seq.map(fun (x,y) -> listContainsItem(l1,x,y))
  18.  
  19.     let itemsetMatches = this.GetC1()
  20.                             |> Seq.map(fun t -> t.Items)
  21.                             |> Seq.map(fun i -> someFunctionINeedToRename(i,itemset))
  22.  
  23.     let itemSupport = itemsetMatches
  24.                             |> Seq.map(Seq.map(fun i -> if i then 1 else 0))
  25.                             |> Seq.reduce(Seq.map2(+))
  26.  
  27.     itemSupport
  28.         |> Seq.zip(itemset)
  29.         |> Seq.toList

So now I have C2 filling correctly:

image

 

Taking the results, I needed to get L2.

image

That was much simpler that getting C2 –> here is the code:

  1. member this.GetL2() =
  2.     let numberOfTransactions = this.GetC1().Count
  3.     
  4.     this.GetC2()
  5.             |> Seq.map(fun (i,n) -> i,n,float n/float numberOfTransactions)
  6.             |> Seq.filter(fun (i,n,p) -> p >= support)
  7.             |> Seq.map(fun (t,i,p) -> t,i)
  8.             |> Seq.sort
  9.             |> Seq.toList    

And when I run it – it matches this example exactly:

image

Finally, I added in a C# and L3.  This code is identical to the C2/L2 code with one exception: mapping a triple and not a tuple:  The C2 code maps like this

  1. let itemset =
  2.     l1Itemset
  3.         |> Seq.map(fun x -> l1Itemset |> Seq.map(fun y -> (x,y)))
  4.         |> Seq.concat
  5.         |> Seq.filter(fun (x,y) -> x < y)
  6.         |> Seq.sort
  7.         |> Seq.toList     

and the C3 code looks like this (took me 15 minutes to figure out line 3 below):

  1. let itemset =
  2.     l2Itemset
  3.         |> Seq.map(fun x -> l2Itemset |> Seq.map(fun y-> l2Itemset |> Seq.map(fun z->(fst x,fst y,snd z))))
  4.         |> Seq.concat
  5.         |> Seq.collect(fun d -> d)
  6.         |> Seq.filter(fun (x,y,z) -> x < y && y < z)
  7.         |> Seq.distinct
  8.         |> Seq.sort
  9.         |> Seq.toList    

With the C3 and L3 matching the example also:

image

image

 

I was now ready to put in the elevator data into the analysis.  I think I am getting better at F# because I did the mapping, filtering, and transformation of the data from the server without looking at any other material and it look only 15 minutes.

  1. type public ElevatorBuilder() =
  2.     let connectionString = ConfigurationManager.ConnectionStrings.["localData2"].ConnectionString;
  3.  
  4.     member public this.GetElevatorTransactions() =
  5.         let transactions = this.GetElevators()
  6.                               |> Seq.map(fun e ->this.ConvertElevatorToTransaction(e))
  7.         let transactionsList = new System.Collections.Generic.List<Transaction>(transactions)
  8.         transactionsList
  9.  
  10.     member public this.ConvertElevatorToTransaction(i: string, t:string, c:string, s:string) =
  11.         let items = new System.Collections.Generic.List<String>()
  12.         items.Add(t)
  13.         items.Add(c)
  14.         items.Add(s)
  15.         let transaction = {TID=i; Items=items}
  16.         transaction
  17.  
  18.     member public this.GetElevators () =
  19.         SqlConnection.GetDataContext(connectionString).ElevatorData201402
  20.             |> Seq.map(fun e -> e.ID, e.EquipType,e.Capacity,e.Speed)
  21.             |> Seq.filter(fun (i,et,c,s) -> not(String.IsNullOrEmpty(et)))
  22.             |> Seq.filter(fun (i,et,c,s) -> c.HasValue)
  23.             |> Seq.filter(fun (i,et,c,s) -> s.HasValue)
  24.             |> Seq.map(fun (i,t,c,s) -> i, this.CatagorizeEquipmentType(t),c,s)
  25.             |> Seq.map(fun (i,t,c,s) -> i,t,this.CatagorizeCapacity(c.Value),s)
  26.             |> Seq.map(fun (i,t,c,s) -> i,t,c,this.CatagorizeSpeed(s.Value))
  27.             |> Seq.map(fun (i,t,c,s) -> i.ToString(),t,c,s)

The longest part was aggregating the free-form text of the Equipment Type field (here is partial snip, you get the idea…)

  1. member public this.CatagorizeEquipmentType(et: string) =
  2.     match et.Trim() with
  3.         | "OTIS" -> "OTIS"
  4.         | "OTIS (1-2)" -> "OTIS"
  5.         | "OTIS (2-1)" -> "OTIS"
  6.         | "OTIS hydro" -> "OTIS"
  7.         | "OTIS, HYD" -> "OTIS"
  8.         | "OTIS/ ASHEVILLE " -> "OTIS"
  9.         | "OTIS/ MOUNTAIN " -> "OTIS"
  10.         | "OTIS/#1" -> "OTIS"
  11.         | "OTIS/#19 " -> "OTIS"

Assigning categories for speed and capacity was a snap using F#

  1. member public this.CatagorizeCapacity(c: int) =
  2.     let lowerBound = (c/25 * 25) + 1
  3.     let upperBound = lowerBound + 24
  4.     lowerBound.ToString() + "-" + upperBound.ToString()        
  5.  
  6. member public this.CatagorizeSpeed(s: int) =
  7.     let lowerBound = (s/50 * 50) + 1
  8.     let upperBound = lowerBound + 49
  9.     lowerBound.ToString() + "-" + upperBound.ToString()    

With this in hand, I created a Console app that takes the 27K records and pushes them though the apriori algorithm:

  1. private static void RunElevatorAnalysis()
  2. {
  3.     Stopwatch stopwatch = new Stopwatch();
  4.     stopwatch.Start();
  5.     ElevatorBuilder builder = new ElevatorBuilder();
  6.     var transactions = builder.GetElevatorTransactions();
  7.     stopwatch.Stop();
  8.     Console.WriteLine("Building " + transactions.Count + " transactions took: " + stopwatch.Elapsed.TotalSeconds);
  9.     var apriori = new Apriori(transactions, .1, .75);
  10.     var c2 = apriori.GetC2();
  11.     stopwatch.Reset();
  12.     stopwatch.Start();
  13.     var l1 = apriori.GetL1();
  14.     Console.WriteLine("Getting L1 took: " + stopwatch.Elapsed.TotalSeconds);
  15.     var l2 = apriori.GetL2();
  16.     Console.WriteLine("Getting L2 took: " + stopwatch.Elapsed.TotalSeconds);
  17.     var l3 = apriori.GetL3();
  18.     Console.WriteLine("Getting L3 took: " + stopwatch.Elapsed.TotalSeconds);
  19.     stopwatch.Stop();
  20.     Console.WriteLine("–L1");
  21.     foreach (var t in l1)
  22.     {
  23.         Console.WriteLine(t.Item1 + ":" + t.Item2);
  24.     }
  25.     Console.WriteLine("–L2");
  26.     foreach (var t in l2)
  27.     {
  28.         Console.WriteLine(t.Item1 + ":" + t.Item2);
  29.     }
  30.     Console.WriteLine("–L3");
  31.     foreach (var t in l3)
  32.     {
  33.         Console.WriteLine(t.Item1 + ":" + t.Item2);
  34.     }
  35. }

I then made an offering to the F# Gods and hit F5:

image

Doh!  The gods were not pleased.  I then went back to my initial filtering function and added a Seq.Take(25000) and the results:

image

So there a couple of things to draw from this exercise.

1) Apriori Algorithm is the wrong classification technique for this dataset.  I had to bring the support way down (10%) to even get any readings.  Also, there is too much dispersion of the values.  This kind of algorithm is much better with N number of a smaller set of data values versus a fixed number of large values.

2) Even so, how cool is this?  Compare the files just to make the C#/OO work versus with F#

imageimage

And the Total LOC is 539 for C# versus 120 for F# – and the F# can be optimized using a better way to create search and itemsets.  Hard-coding each level was a hack I did to get thing working and give me an understanding of how AA works.  I bet this can be consolidated to well under 75 lines without sacrificing readability

3) I think the StackOverflow exception is because I am doing a Cartesian join and then paring the result.  Using one of the other techniques suggested on SO will give much better results.

I any event, what a fun project!  I can’t wait to optimize this and perhaps throw a different algorithm at the dataset in the coming weeks.

 

 

 

Elevator App: Part 1 – Data Layer Using F#

 

At Open Data Day, fellow TRINUGER Elaine Cahill told me about a website where you can get all of the elevator inspection data for the state.  It is found here.  She went ahead and put the Wake County data onto Socrata.  I wanted to look at the entire state so I went to the report page like so:

 

image

Unfortunately, when you try and pull down the entire state, you cause a server exception:

 

image

 

So I split the download in half.  I then Imported it into Access and then SSISed it into Azure Sql.  I then created a project to server the data and I decided to use F# type providers as a replacement for Entity Framework for my ORM.  I could either use the SqlEntity TP or the SqlDataConnection TP to access the Sql Database on Azure.  Both do not work out of the box.

SqlDataConnection

I could not get  SqlDataConnection to work at all.  When I hooked it up to a standard connection string in the config file, I got:

image

So when I copy and paste the connection string into the TP directly, it does make the connection to Azure, but then it comes back with this exception:

image

Without looking at the source. my guess is that the TP has hard-coded a reference to ‘syscomments’ and alas, Azure does not have that table.

SqlEntity

I then headed over to the SlqEntityTP to see if I could have better luck.  Fortunately, the SqlEntity does work with both an Azure connection string in the .config file and can make a connection to an Azure database.

The problem I ran into was when I wanted to expose the SqlConnection the the WebAPI project that I wrote in C#.  You can not mark SqlEntityTPs as public:

image

Note that the SqlDataConnection can be marked as public. <sigh>.  I marked the SqlEntityTP as internal and then created a POCO to map between the SqlEntity type and a type that can be consumed by the outside world:

  1. type public Elevator ={
  2.         ID: int
  3.         County: string
  4.         StateId: string
  5.         Type: string
  6.         Operation: string
  7.         Owner: string
  8.         O_Address1: string
  9.         O_Address2: string
  10.         O_City: string
  11.         O_State: string
  12.         O_Zip: string
  13.         User: string
  14.         U_Address1: string
  15.         U_Address2: string
  16.         U_City: string
  17.         U_State: string
  18.         U_Zip: string
  19.         U_Lat: double
  20.         U_Long: double
  21.         Installed: DateTime
  22.         Complied: DateTime
  23.         Capacity: int
  24.         CertStatus: int
  25.         EquipType: string
  26.         Drive: string
  27.         Volts: string
  28.         Speed: int
  29.         FloorTo: string
  30.         FloorFrom: string
  31.         Landing: string
  32.         Entrances: string
  33.         Ropes: string
  34.         RopeSize: string
  35.     }
  36.  
  37. type public DataRepository() =
  38.     let connectionString = ConfigurationManager.ConnectionStrings.["azureData"].ConnectionString;
  39.  
  40.     member public this.GetElevators () =
  41.         SqlConnection.GetDataContext(connectionString).ElevatorData201402
  42.         |> Seq.map(fun x -> this.GetElevatorFromElevatorData(x))
  43.  
  44.     member public this.GetElevator (id: int) =
  45.         SqlConnection.GetDataContext(connectionString).ElevatorData201402
  46.         |> Seq.where(fun x -> x.ID = id)
  47.         |> Seq.map(fun x -> this.GetElevatorFromElevatorData(x))
  48.         |> Seq.head
  49.  
  50.     member internal this.GetElevatorFromElevatorData(elevatorData: SqlConnection.ServiceTypes.ElevatorData201402) =
  51.         let elevator = {ID= elevatorData.ID;
  52.             County=elevatorData.County;
  53.             StateId=elevatorData.StateID;
  54.             Type=elevatorData.Type;
  55.             Operation=elevatorData.Operation;
  56.             Owner=elevatorData.Owner;
  57.             O_Address1=elevatorData.O_Address1;
  58.             O_Address2=elevatorData.O_Address2;
  59.             O_City=elevatorData.O_City;
  60.             O_State=elevatorData.O_St;
  61.             O_Zip=elevatorData.O_Zip;
  62.             User=elevatorData.User;
  63.             U_Address1=elevatorData.U_Address1;
  64.             U_Address2=elevatorData.U_Address2;
  65.             U_City=elevatorData.U_City;
  66.             U_State=elevatorData.U_St;
  67.             U_Zip=elevatorData.U_Zip;
  68.             U_Lat=elevatorData.U_lat;
  69.             U_Long=elevatorData.U_long;
  70.             Installed=elevatorData.Installed.Value;
  71.             Complied=elevatorData.Complied.Value;
  72.             Capacity=elevatorData.Capacity.Value;
  73.             CertStatus=elevatorData.CertStatus.Value;
  74.             EquipType=elevatorData.EquipType;
  75.             Drive=elevatorData.Drive;
  76.             Volts=elevatorData.Volts;
  77.             Speed=int elevatorData.Speed;
  78.             FloorTo=elevatorData.FloorTo;
  79.             FloorFrom=elevatorData.FloorFrom;
  80.             Landing=elevatorData.Landing;
  81.             Entrances=elevatorData.Entrances;
  82.             Ropes=elevatorData.Ropes;
  83.             RopeSize=elevatorData.RopeSize
  84.         }
  85.         elevator

I am not happy about writing any of this code.  I have 84 lines of code for a single class.  I might have well used the code code gen of EF.  I could have taken the performance hit and used System.Reflection to map field of the same names (I have done that on other projects) , but that also feels like a hack.   In any event, I then added a reference to my F# project in my C# WebAPI project.  I did have to add a reference to FSharp.Core in the C# project (which further vexed me), but then I created a couple of GET methods to expose the data:

 

  1. public class ElevatorController : ApiController
  2. {
  3.     // GET api/Elevator
  4.     public IEnumerable<Elevator> Get()
  5.     {
  6.         DataRepository repository = new DataRepository();
  7.         return repository.GetElevators();
  8.     }
  9.  
  10.     // GET api/Elevator/5
  11.     public Elevator Get(int id)
  12.     {
  13.         DataRepository repository = new DataRepository();
  14.         return repository.GetElevator(id);
  15.     }
  16.  
  17. }

 

When I viewed the JSON from a handy browser, it looks like, well, junk:

image

So now I have to get rid of that random characters (x0040 suffix)– yet a 3rd POCO, this one in C#:

  1. public class ElevatorController : ApiController
  2. {
  3.     // GET api/Elevator
  4.     public IEnumerable<CS.Elevator> Get()
  5.     {
  6.         List<CS.Elevator> elevators = new List<CS.Elevator>();
  7.         FS.DataRepository repository = new FS.DataRepository();
  8.         var fsElevators = repository.GetElevators();
  9.         foreach (var fsElevator in fsElevators)
  10.         {
  11.             elevators.Add(GetElevatorFromFSharpElevator(fsElevator));
  12.         }
  13.         return elevators;
  14.     }
  15.  
  16.     // GET api/Elevator/5
  17.     public CS.Elevator Get(int id)
  18.     {
  19.         FS.DataRepository repository = new FS.DataRepository();
  20.         return GetElevatorFromFSharpElevator(repository.GetElevator(id));
  21.     }
  22.  
  23.     internal CS.Elevator GetElevatorFromFSharpElevator(FS.Elevator fsElevator)
  24.     {
  25.         CS.Elevator elevator = new CS.Elevator();
  26.         elevator.ID = fsElevator.ID;
  27.         elevator.County = fsElevator.County;
  28.         elevator.StateId = fsElevator.StateId;
  29.         elevator.Type = fsElevator.Type;
  30.         elevator.Operation = fsElevator.Operation;
  31.         elevator.Owner = fsElevator.Owner;
  32.         elevator.O_Address1 = fsElevator.O_Address1;
  33.         elevator.O_Address2 = fsElevator.O_Address2;
  34.         elevator.O_City = fsElevator.O_City;
  35.         elevator.O_State = fsElevator.O_State;
  36.         elevator.O_Zip = fsElevator.O_Zip;
  37.         elevator.User = fsElevator.User;
  38.         elevator.U_Address1 = fsElevator.U_Address1;
  39.         elevator.U_Address2 = fsElevator.U_Address2;
  40.         elevator.U_City = fsElevator.U_City;
  41.         elevator.U_State = fsElevator.U_State;
  42.         elevator.U_Zip = fsElevator.U_Zip;
  43.         elevator.Installed = fsElevator.Installed;
  44.         elevator.Complied = fsElevator.Complied;
  45.         elevator.Capacity = fsElevator.Capacity;
  46.         elevator.CertStatus = fsElevator.CertStatus;
  47.         elevator.EquipType = fsElevator.EquipType;
  48.         elevator.Drive = fsElevator.Drive;
  49.         elevator.Volts = fsElevator.Volts;
  50.         elevator.Speed = fsElevator.Speed;
  51.         elevator.FloorTo = fsElevator.FloorTo;
  52.         elevator.FloorFrom = fsElevator.FloorFrom;
  53.         elevator.Landing = fsElevator.Landing;
  54.         elevator.Entrances = fsElevator.Entrances;
  55.         elevator.Ropes = fsElevator.Ropes;
  56.         elevator.RopeSize = fsElevator.RopeSize;
  57.         return elevator;
  58.     }
  59.  
  60. }

 

So that gives me that I want…

image

As a side note, I learned the hard way that the only way to force the SqlEntityTP to update based on a schema  change in the DB is to change the connection string in the .config file

Finally, when I published the WebAPI project to Azure, I got an exception. 

<Error><Message>An error has occurred.</Message><ExceptionMessage>Could not load file or assembly 'FSharp.Core, Version=4.3.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.</ExceptionMessage><ExceptionType>System.IO.FileNotFoundException</ExceptionType><StackTrace> at System.Web.Http.ApiController.<InvokeActionWithExceptionFilters>d__1.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__0.MoveNext()</StackTrace

Turns out you need to not only add a reference to the F# project and FSharp.Core, you have to deploy the .dlls to Azure also.  Thanks to hocho on SO for that one.

In conclusion, I love the promise of TPs.  I want nothing more than to throw away all of the EF code-gen, .tt files, seeding for code-first nonsense, etc… and replace it with a single line TP.  I have done this on a local project, but when I did it with an Azure, things were harder than they should be.  Since it is easier to throw hand grenades than catch them, I made a list of the things I want to help the open source FSharp.Data project accomplish in the coming months:

1) SqlDatabaseConnection working with Azure Sql Storage

2) MSAccessConnection needed

3) ActiveDirectoryConnection needed

4) Json and WsdlService ability to handle proxies

5) SqlEntityConnection exposing classes publicly

Regardless of what the open-source community does, MSFT will still have to make a better commitment to F# on Azure, IMHO…

Elevator App: Part 0

When I was young, I used to have window races with my siblings in the car. In those days, windows would be manual with a crank like this:

clip_image001

Each kid would get a door and on “go”, we would crank, crank, crank the window all the way up and all the way down. These were great time passers on a long trip – until dad noticed that it was getting alternately windy and calm in the car and put an end to it. With the advent of electric motors for the windows in the car, the races became much closer – we honestly thought that harder you pushed on the button would cause the window move up and down faster. It was good fun until dad caught me pouring Crisco down the back left window of the Buick Skylark.  Greasing the wheels, if you will.

Fast forward 15 years when I was just out of school at my 1st job in San Francisco. One of the things we did when we didn’t have any money (often) on a Friday night was to ride elevators. Find a nice building, walk in like you knew what you were doing, and ride to the top and back down. Hotels were the best – ideally you could get free food in the bar for happy hour too – it was dinner theatre. Sometimes, if there were two elevators in the bank, we would turn it into a competition – pick your elevator wisely. If someone got on your elevator while you were racing, you were out of luck. This added a good deal of tension to the race. You were in the lead on the way back down but then someone stopped you on floor 2 to go down to G. “UGHHHHH. Take the stairs fat-ass!”

(BTW: how great would this be for a plot of a movie? The main characters pick different building to race until one night, they picked the wrong one….)

The biggest problem with elevator races (outside of getting caught and spending a night in jail), was that if the building only had 1 elevator, you really couldn’t race. Even if you had a stopwatch, you really didn’t know if the person made it to the top. Also, the chance of other people who were waiting stopping your elevator was now 100%.  Plus, do you really trust your competition with the stop-watch?  Maybe your friends, but not mine…

Fast forward 20 more years and I started working with open data. One of the data sets that TRINUG wanted to look at was elevator inspection data. When I went to the website where you can do reporting and there was a column called…. speed. Holy smokes, we can see the speed of the different elevators in town! I then thought about a phone app that could measure the speed of the elevator versus the reported speed. Also, we finally have a solution to the 1-elevator race problem! I then thought about how to tie this into coming up to speed with the latest javascript technologies (I am trying to get my MCSD in Web too).  Thus, I am going to create an elevator speed app using Angular and Phonegap.  Let’s see how it goes…

Restaurant Classifier: Async For Faster Performance?

Going back to my restaurant classifier using F# from last week, I decided to speed things up some.  Each request to the Yellow Pages API takes 1 second, so with the 5,682 records, I am looking at a little over 1.5 hours to pull down the data when running serial.

I first thought about making my methods async so I changed the API call method to async and used the Http.AsyncRequest method like so (line 10 below):

  1. member this.GetCatagoriesAsync(restaurantName: string, restaurantAddress: string) =
  2.          async{
  3.              if(String.IsNullOrEmpty(restaurantName)) then
  4.                  failwith("restaurantName cannot be null or empty.")
  5.              if(String.IsNullOrEmpty(restaurantAddress)) then
  6.                  failwith("restaurantAddress cannot be null or empty.")
  7.              let cleanedName = this.CleanName(restaurantName)
  8.              let cleanedAddress = this.CleanAddress(restaurantAddress);
  9.              let uri = "http://pubapi.atti.com/search-api/search/devapi/search?term=&quot;+cleanedName+"&searchloc="+cleanedAddress+"&format=json&key=qj5l8pphj5"
  10.              let! response = FSharp.Net.Http.AsyncRequest(uri, headers=["user-agent", "None"])
  11.              let ypResult = ypProvider.Parse(response)
  12.              try
  13.                  return ypResult.SearchResult.SearchListings.SearchListing.[0].Categories
  14.              with
  15.                  | ex -> return String.Empty
  16.          }

I then made the covering function async also (line 11 below)

  1. member this.IsRestaurantInCatagoryAsync(restaurantName: string, restaurantAddress: string, restaurantCatagory: string) =
  2.     async {
  3.         if(String.IsNullOrEmpty(restaurantName)) then
  4.             failwith("restaurantName cannot be null or empty.")
  5.         if(String.IsNullOrEmpty(restaurantAddress)) then
  6.             failwith("restaurantAddress cannot be null or empty.")
  7.         if(String.IsNullOrEmpty(restaurantCatagory)) then
  8.             failwith("restaurantCatagory cannot be null or empty.")
  9.  
  10.         System.Threading.Thread.Sleep(new System.TimeSpan(0,0,1))
  11.         let! catagories = this.GetCatagoriesAsync(restaurantName, restaurantAddress)
  12.         if(String.IsNullOrEmpty(catagories)) then return false
  13.         else return this.IsCatagoryInCatagories(catagories,restaurantCatagory)
  14.     }

The problem is that invoking the covering function via an anonymous method did not work easily.

image

After screwing around with the synax a bit, I went over to stack overflow where I found out two things:

  • There is not an easy way to do it (I was hoping for a Seq.FilterAsyc method)
  • Thomas Petricek is above my pay-grade. 

In any event, I decided to drop the async and just look at parallelism.   Turns out that there is a Parallel Seq class called PSeq, it is just not in the FSharp core library yet.   I created a PSeq file in my project, moved it to the top and dropped the code in.   I then changed the method call to use PSeq to invoke the serial methods:

  1. member public this.GetChineseRestaurants () =
  2.     let catagoryRepository = new RestaurantCatagoryRepository()
  3.     let catagory = "Chinese"
  4.     this.GetRestaurants()
  5.             |> PSeq.filter(fun (name, address) -> catagoryRepository.IsRestaurantInCatagory(name, address,catagory))
  6.             |> Seq.toList    

When I first invoked it and looked at Fiddler (OT: did anyone notice that Fiddler’s new logo looks alot like a FSharp one?  Probably just a coincidence), it was clear that things were running in parallel and that performance would improve.  I have two cores on this workstation so my time be cut in half. 

image

With the parallel method in my back pocket, I decided to see the ultimate result of the restaurant classification.  I created a quick console app

  1. class Program
  2. {
  3.     static void Main(string[] args)
  4.     {
  5.         Console.WriteLine("Start");
  6.  
  7.         Stopwatch stopwatch = new Stopwatch();
  8.         stopwatch.Start();
  9.         RestaurantBuilder builder = new RestaurantBuilder();
  10.         var restaurants = builder.GetChineseRestaurants();
  11.         
  12.         foreach (var restaurant in restaurants)
  13.         {
  14.             Console.WriteLine(restaurant.Item1 + ":" + restaurant.Item2);
  15.         }
  16.         
  17.         stopwatch.Stop();
  18.         Console.WriteLine("Number of Chinese Restaurants: " + restaurants.Count());
  19.         Console.WriteLine(stopwatch.Elapsed.ToString());
  20.         Console.WriteLine("End");
  21.         Console.ReadKey();
  22.     }
  23. }

I then ran the search on YP.com using my 4 core laptop and got the following results:

image

Compared to my original classifier based on name:

image

So the results make sense.  The YP serial search would take at least 94.7 minutes, the YP parallel search took 41 minutes, and the in-memory name search took 3 seconds.  The YP search(s) found restaurants that the name did not (Wang’s Kitchen, Crazy Fire Mongolian Grill, etc…) – 275 to 221, or 24% more restaurants.

I think that the next step is to look at the classifier and see how many restaurants are in both datasets and why the ones that are not in the YP one – where they are (did they even pay to be in the Yellow Pages?).  Perhaps there is another YP category that can be considered.  Also, it would be interesting to see of the restaurants that are in the name search and in the Yellow Pages that were not classified as Chinese – the false positive rate.  Finally, I did see some 500s in Fiddler that had “read time out” so there is room for improvement to account for the transient faults…

Follow

Get every new post delivered to your Inbox.