Moved

Moved. See https://slott56.github.io. All new content goes to the new site. This is a legacy, and will likely be dropped five years after the last post in Jan 2023.

Tuesday, December 20, 2016

The Royal Road

Warning: Long Boring Anecdote: Conclusions will be drawn from a single example.

First, this quote:
"All your suggestions were great if I had wanted to do a systematic study and truly understand. The goal is to understand and learn as little as possible to be able to undertake the code challenge for [a specific job opportunity]."
The subject of systematic study is Python. The focus of learn as little as possible is Pandas.

The goal is more-or-less impossible. Focus on a specific code challenge will devolve into other aspects of the language. Or. It will lead nowhere.

Also. I'm not sure what a "systematic study" is. From the omitted back-story, I'm seems clear to me that the advice to "read a tutorial" is restated here as "systematic study." And is unacceptable. I guess because of time constraints.

It gets better.

There's this:
"For now, I am just going to follow the pattern and get stuff to work."
This is the ideal way to be defeated by technology. The "pattern" is defined to a large degree by the programming language. The two are inextricably linked. Trying to identify a pattern in programming that's magically not part of the implementation language seems deranged.

Learning Pandas isn't simple. There's no royal road.

The real crux are several questions that are difficult to reproduce, so I'm forced to summarize.

  • What does name.name().name().name() mean? How can you call multiple methods "simultaneously"?
  • What is the range of values for some parameter? 

And the capper.

  • I understand object.method().method().  I cannot understand object.name -- how can a method have no ()'s?
I'm sorry, but, the advice still stands. These are not questions that can be answered in a vacuum. This is serious -- and foundational -- object-oriented programming. Each of these small things was a total show-stopper, leading to four emails merely to clarify the question. Then more when the answer was rejected as not consistent with something, or astonishing, or 

For the name.name().name(), the answer "Google Fluent Interface" was too complex. Code examples were requested to show how it was possible for an object to return another object that had methods. 

Advice to use Python's >>> prompt and the dir() function were apparently part of "systematic study" and not part of "learn as little as possible."

For the range of values of a parameter, the answer "read the source" was a non-starter. Reading the source was flat-out rejected as not making any sense. I finally had to actually provide the link to the source repo for Pandas before it became clear what "open source" even meant. This was found to be nothing short of astonishing. The side-bar conversation on "how is this even possible?" was confusing to me because -- sadly -- I assumed people knew that the words "open" and "source" together meant that the source was open for inspection. My assumption was wrong. At least one person did not know this. That means there are others.

Finally. For the "capper" question. The exchange really did include this: "The dot notation I thought was Object.Method.Method."

A great deal of the back-and-forth amounted to "I reject anything other than fluent methods because that's the only thing I've decided to understand." Words like "property" and "attribute" were ignored as noise, AFAIK.

I say "amounted to" because a lot of the back-and-forth was restating the question. Other parts were exchanging links to the Pandas documentation in an effort to follow the "learn as little as possible" strategy. Any link to a tutorial would be "systematic study". Any link to Pandas, however, was acceptable. But (of course) confusing because the Pandas documentation assumes a modicum of language knowledge.

Here's what appears to be the problem: it's impossible to learn a complex tool like Pandas without starting with a basic understanding of Python. 

I don't think I've ever seen it suggested that one can leap into a package without knowing the language. I'm not sure how one can develop the idea of learning as little as possible in the first place. But, there it is. 

There's at least one person who thinks they can learn as little as possible and still get Pandas code to work. That likely means there are others.

It appears there's a market for books like 

Learn as little Python as possible to be able to use Pandas

and

Learn the least Java necessary to make Spark work

and

The Royal Road to Data Science

I'm not sure I'm capable of writing books like these, but for someone who does, it might be a really lucrative line of books.

Tuesday, December 13, 2016

Amazing how Windows is “special.”

Here's the quote:
...it is amazing how Windows is “special.” Back when ..., special things had to be done for Windows. Python continues the tradition w/ an entire section in its doco titled “3. Using Python on Windows”
I wasn't sure what to make of this.

It appeared that they want Windows to be the norm and Linux/Mac OS/POSIX to be treated as an exception.

I find that baffling. POSIX is a standard. Mac OS X is POSIX-compliant. Linux distros are generally POSIX-compliant. There's a nice list: https://en.wikipedia.org/wiki/POSIX

Windows is not POSIX-compliant. It seems to me that non-standard == special shouldn't be "amazing." It should be "tiresome" or "annoying."

Windows tools are uniquely awful. Or, perhaps, "special". Windows is so "special" that the IDE concept appears to have evolved as a solution to the awfulness. Using a Windows IDE (like Visual Studio) insulated one from the vagaries of Windows. It appears this is particularly important when trying to create a binary that will work in multiple incarnations of Windows.

In Linux (and POSIX-compliant OS's in general) the OS is the IDE. Start here: https://sanctum.geek.nz/arabesque/unix-as-ide-introduction/.  This seems so much simpler and more rational. Perhaps I'm just biased because I've used so many OS's that aren't Windows.

Worth considering: http://www.psychocats.net/ubuntu/virtualbox

When asked about IDE's for Python, I tell people that I've used a number of text editors to write Python code:
  • vi
  • BBEdit
  • Atom
  • Komodo Edit
  • Notepad++
  • PyCharm
  • IDLE
  • Jupyter Notebook
They all work nicely. It's difficult to recommend one because they all have distinct features. I always wind up with a lot of command-line interaction. The "run-a-command-from-the-IDE" has complex dialog boxes and sometimes confusing limitations. It's easier to simply write a script than discern the nuances of the IDE configuration rules.

These are (mostly) platform-independent. They can minimize a few of the Windows "features." They don't eliminate all of the Windows issues.

In all cases -- except using IDLE -- I also have a Python `>>>` prompt open in a terminal window.  

I strongly encourage everyone to work this way. The terminal window interaction can be copied and pasted into doctest strings. You've written a unit test without really trying. It's extremely productive. It gets away from IDE wrappers. It does expose some Windows-isms, but as long as you can limit the number of times you find Windows "amazing," that's not a problem.

Tuesday, November 29, 2016

A Reason for Avoiding Programming

From someone in the process of becoming a data scientist. They had a question on regular expressions, which made almost no sense. It appears that the core concepts of ETL -- Extracting source data, Transforming it into a useful form and the Loading into some persistent storage for long-term analysis -- had not been embraced. It appears the design pattern was unknown. All I could gather from the sketchy email chain was that something involving regular expressions had become difficult.

I wrote this in response: Handling Irregular File Formats.

Here's part of the follow-up.

"I have been focusing on the math associated w/ math optimization. I have been using spreadsheets to perform the computations."

Really.

Spreadsheets.

The ETL pipeline question/rant/complaint was part of loading a spreadsheet?

That seems somehow wrong. There are real tools available that really do real data science work. The word "optimization" hints that scipy.optimize might be a more useful exercise than hacking around with spreadsheets.

Perhaps some advice from a real data scientist might help: http://www.becomingadatascientist.com

Tuesday, November 22, 2016

The Modern Python Cookbook

See https://www.packtpub.com/application-development/modern-python-cookbook

This is a large (!) collection of recipes, focused on Python 3, exclusively.

It's much easier to write about the version of Python I actually use each day, and leave the old, quirky, slow Python 2 behind. This book doesn't have any "this will be different in Python 2" warnings. Those days seem to have passed. Finally.

The clock is counting down. https://pythonclock.org


Wednesday, November 16, 2016

Tuesday, November 1, 2016

Handling Irregular File Formats

This is a common issue. We have a file which was printed for human consumption. Consequently, it has many different kinds of lines.

These are the two kinds of lines of interest:

900296268 4/9/16 Mobility, Data Mining and Privacy Expired

900295204 4/1/16 Pro .NET Best Practices
Expired

The first is a single physical line.  It has four data elements. The second is two physical lines. The first has three data elements.

There are a number of other noise lines in the file which must be filtered out.

The first "solution" pitched to me could be summarized with this:

Move "Expired" on a line by itself to the previous line

That was part of the email subject line. The body of the email was some whining about regular expressions. Which I mostly ignored. Multiline regular expressions are their own kind of challenge.

We (should) all know this: https://blog.codinghorror.com/regular-expressions-now-you-have-two-problems/

Let's do this without regular expressions. There are two things we need to know. One is buffering, and the other is the best way to split each line. It turns out that there are spaces as well as tabs, and can can, by splitting on tabs, make a lot of progress.

Instead of the good approach, I'll pick the other approach that doesn't involve splitting on tabs.

Here's the simulated file, with data lightly redacted.

sample_text = '''
"Your eBooks"

Show 200



Page: 1



Order # Date Title Formats Status Download
-------
xxx315605 9/30/16 R for Cloud Computing Available



xxx304790 6/21/16 Java XML and JSON Available
xxx304790 6/21/16 Accelerated DOM Scripting with Ajax, APIs, and Libraries Available

xxx291633 2/28/16 Practical Google Analytics and Google Tag Manager for Developers
Expired
'''

It's not perfectly obvious (because of line wrapping) but there are three examples of the "all-complete-in-one-line" records. There's one example of the "two-lines" record.

Rather than mess with the file, we'll build a file-like object with our sample data.

import io
file_like_object = io.StringIO(sample_text)

I like this because it lets me write proper unit test cases.

The file has four kinds of lines:

  • Complete Records
  • Record Headers (without Available/Expired)
  • Record Trailers (only Available/Expired)
  • Noise

We'll create some decision rules for the two obvious kinds of file lines: complete records and trailers. We can deduce the headers based on a simple adjacency rule: they precede a trailer. The fourth kind of lines are those which are possible headers but are not immediately prior to a trailer.

def complete(words):
    return len(words) > 3 and words[-1] in ('Available', 'Expired')

def trailer(words):
    return len(words) == 1 and words[0] in ('Available', 'Expired')    

We can spot these two kinds of lines easily. The other kinds require a Buffered Generator.

def emit_clean(source):
    header = None
    for line in (line.strip() for line in source):
        words = [w.strip() for w in line.split()]
        if len(words) == 0: continue
        if complete(words):
            yield(line)
            header = None
        elif trailer(words) and header:
            yield(header + '\t\t' + line)
            header = None
        else:
            # Possible header
            # print('??', line)
            header = line

The Buffered Generator is a way to implement a "look ahead one item" (LA1) algorithm. We do this by buffering rows. When we get to the next row we can use the buffered row and the current row to implement the look-ahead logic.

The actual implementation uses a look-behind buffer, header.

The (line.strip() for line in source) generator expression strips away leading and trailing spaces. This gets rid of the newline characters at the end of each input line.

The default behavior of split() is to split on whitespace. In this case, it will create a number of words for complete records or header records, and a single word for a trailer record. If we had split on tab characters, some of this logic would be simplified.

That's left as an exercise for the reader.

If the len(words) is zero, the line is blank.

If the line matches the complete() function, we can yield it as one of the iterable results of the generator function. We also clear out the look-behind buffer, header.

If the line is a trailer and we have a buffered look-behind line, this is the two-physical-line case. We can assemble a complete record and emit it.

Otherwise, we don't know what the line is. It's a possible header line, so we'll save it for later examination.

This algorithm involves no regular expressions.

With Regular Expressions


An alternative would use three regular expressions to match the three kinds of lines.

import re
all_one_pat = re.compile("(.*)\t(.*)\t(.*)\t\t((?:Available)|(?:Expired))")
header_pat = re.compile("(.*)\t(.*)\t(.*)")
trailer_pat = re.compile("((?:Available)|(?:Expired))")

This has the advantage that we can then use the groups() method of each successful match to emit useful data instead of text which needs subsequent parsing. This leads to a slightly more robust process.

def emit_clean2(source):
    header = None
    for line in (line.strip() for line in source):
        if len(line) == 0: continue
        all_one_match = all_one_pat.match(line)
        header_match = header_pat.match(line)
        trailer_match = trailer_pat.match(line)
        if all_one_match:
            yield(all_one_match.groups())
            header = None
        elif header_match and not header:
            header = header_match.groups()
        elif trailer_match and header:
            yield header + trailer_match.groups()
            header = None
        else:
            pass # noise

The essential processing involves seeing which of the regular expressions match the line at hand. If it's all-in-one, this is good. We can yield the groups of meaningful data. If it's a header, we can save the groups. If it's a trailer, we can combine header and trailer groups and yield the composite.

This has the advantage of explicitly rejecting noise lines instead of treating each noise line as a possible header.