Showing posts with label python. Show all posts
Showing posts with label python. Show all posts

Tuesday, May 20, 2014

A Better Bank Statement

Regular bank statements are presented in a very unhelpful way - simply sorted by date, with no analysis of the items to assist you to understand them.

View my Better Bank Statement.ipynb in nbviewer or download the .ipynb file.

The iPython Notebook uses Pandas to analyse your bank statement (downloaded as a .csv) and produces output like the following.

New items that haven't been seen before are separated out so they can be checked easily:


Items to payees that have been seen before but with amounts that are different are displayed graphically to let you see at a glance which are truly problematic and which are normal variations.


Recurring items are usually going to be fine, but you may want to check that an automatic payment hasn't been missed:


The same breakdown is done for credits.

You have to ask yourself: why can't the bank do something like this? Until then, see how you go playing with this notebook.

Saturday, March 17, 2012

Adding "Search at point" function to Notepad++

Notepad++ with the Python Script plugin is a great code editor for windows.

After adding the following two files to your Notepad++ scripts folder you can make it even better with what I'm quickly finding to be an indispensable feature:

By using the Alt+left and Alt+right keys you can move to the previous and next occurrences of the symbol under the cursor. e.g. you can move quickly between all usages of a variable or function name in a file.

# goto_next_occurrence.py
"""Move cursor to next occurrence of the current word in the file, wrap around if possible."""

from Npp import editor, FINDOPTION
symbol = editor.getCurrentWord()
editor.wordRight()
editor.searchAnchor()
pos = editor.searchNext(FINDOPTION.MATCHCASE+FINDOPTION.WHOLEWORD+FINDOPTION.WORDSTART, symbol)
editor.scrollCaret()
# goto_prev_occurrence.py
"""Move cursor to previous occurrence of the current word in the file, wrap around if possible."""

from Npp import editor, FINDOPTION
symbol = editor.getCurrentWord()
editor.wordLeft()
editor.searchAnchor()
pos = editor.searchPrev(FINDOPTION.MATCHCASE+FINDOPTION.WHOLEWORD+FINDOPTION.WORDSTART, symbol)
editor.scrollCaret()

Then add them to the menu:

And bind them to keys, Alt-right and Alt-left work well for me:

Back in Linux-land, there are a bunch of ways to add similar functionality to Emacs, I like this one.

Saturday, December 04, 2010

Python+Emacs made easy with emacs-for-python

I don't know where this has been all my life but Gabriele Lanaro has put together a really easy to use package for adding lots of Python goodies to Emacs.

Just unzip the archive into ~/.emacs.d/ and add one line to your .emacs and your done (well, also remove all the now unnecessary random cargo-cult prior additions to your .emacs):

(load-file "~/.emacs.d/emacs-for-python/epy-init.el")

I had also previously installed a handful of Python/Emacs packages via Synaptic that may or may not be required to make it all work: python-rope, python-ropemacs, pyflakes.

Saturday, October 11, 2008

Merging GPS logs and mapping them all

Inspired by cabspotting and Open Street Map, I wanted to merge all my GPS logs and create a map showing all the routes I've logged lately.

This is pretty easy using gpsbabel, but I needed to use a little Python to get the list of input log files. (I'm sure there's a way to do it in bash but that's beyond me for now.) My GPS stores files in nmea format, and the directory structure/purpose of my Python script should hopefully be apparent.

>>> import os
>>> from path import path
>>> logs = " ".join([" ".join(["-i nmea -f %s"%log
                               for log in sorted((raw/"raw").files("GPS_*.log"))]) 
                     for raw in path("/home/tom/docs/gpslogs").dirs() 
                     if raw.namebase.isdigit()])
>>> logs
'-i nmea -f /home/tom/docs/gpslogs/200810/raw/GPS_20080930_221152.log -i nmea -f /home/tom/docs/gpslogs/200810/raw/GPS_20081001_071234.log ...'
>>> os.system("gpsbabel %s -o kml,points=0,labels=0,trackdata=0 -F /home/tom/docs/gpslogs/all200810.kml" % logs)

The result of that is a 36.5 MB kml file I could load into Google Earth:

There was one spurious point somewhere in the log file at 0° E, 0° N, and the log has a lot of jitter when I'm walking near home.

Monday, August 25, 2008

Analysing GPS Logs with Awk

This post describes the first two "chop" functions that fit into the partitioning framework outlined last post.

def chopToSpeedHistogram(dest, p):
    # create histogram of speeds from nmea written to stdout
    os.system("cat "+sh_escape(dest)+".log"
              + " | awk -F , '{if($1==\"$GPVTG\" && int($8)!=0){count[int($8+0.5)]++}}"
              + " END {for(w in count) printf(\"[%d,%d],\\n\", w, count[w]);}'"
              # sort it
              + " | sort -g -k 1.2"
              # output json of histogram
              + " > "+sh_escape(dest)+".hist")

def chopToHeadingHistogram(dest, p):
    # create histogram of headings from nmea written to stdout (ignore heading when stopped)
    os.system("cat "+sh_escape(dest)+".log"
              + " | awk -F , '{if($1==\"$GPVTG\" && int($8)!=0){count[5.0*int($2/5.0+0.5)]++;}}"
              + " END {for(w in count) printf(\"[%d,%d],\\n\", w, count[w]);}'"
              # sort it
              + " | sort -g -k 1.2"
              # output json of histogram
              + " > "+sh_escape(dest)+".head")

Both functions use awk to create a histogram from the speed (in km/h) and heading (or bearing, in degrees) from the NMEA VTG sentences. The speed is rounded to an integer, and the bearing to the nearest 5 degrees. The data logger records on reading per second, so this gives a measure of how much time was spent at each speed/bearing.

The histogram is output in a "json" array format that can be inserted straight into a webpage where the flot library is used to generate some graphs.

Speed Histogram

The average and standard deviation (shaded at ±0.5σ) are indicated on the graph for two bike rides along the same route, and match pretty closely with that recorded by my bike computer:

GPS logBike computer
Ride 1 (brown) 2hrs 59 min, minus 41 min stopped63.4km27.7km/h 2 hrs 16 min64.01km28.00km/h
Ride 2 (dark green) 2 hrs 25 min, minus 13 min stopped63.4km29.0km/h 2 hrs 10 min63.85km29.30km/h

The two rides went in different directions, the first in the "uphill" direction and the second with a bit of a tail wind. I got a flat tire on the first ride too, hence the extra time spent stopped.

Heading Histogram

Up is north and the radius represents the time spent heading in that direction (normalized during the plotting process and "expanded" by taking the square root to show a little more detail.)

Thursday, August 21, 2008

Automatically Partitioning GPS Logs with gpsbabel

My GPS logger is capturing lots of useful information but it's difficult to efficiently capture data for regular activities. Geotagging photos is easy, and manually working with the logs for a special event is possible, but it's not feasible to put in that much work to analyze commutes for example.

The logger creates a separate log file each time it's switched on and off, and while these logs could be sorted into categories for analysis, it's easy to forget to turn it on and off at the start and end of a section of interest and activities are then merged in the logs. In addition, there is often "junk" data at start and end of logs while leaving or arriving at a destination.

I wanted to be able to automatically capture the information about my daily activities by simply switching on the logger and carrying it around with me. I then simply want to plug the logger into the computer and have the logs automatically chopped into segments of interest that can be compared to each other over time.

The rest of this post roughly outlines the Python script I created to perform this task, minus some of the hopefully irrelevant details.

Firstly, I collect the lat/long coordinates of places that I am interested in collecting data while I'm there and traveling between them. These include my home, work, the climbing gym and so on. Each point has a radius within which any readings will be considered to be in that place.

#         id:  name lat         long        radius
places = { 1: ("A", -37.123456, 145.123456, 0.050),
           2: ("B", -37.234567, 145.234567, 0.050),
           3: ("C", -37.345678, 145.345678, 0.050) }
otherid = 4

For each of these places of interest, I then use gpsbabel's radius filter to find all the times where I was within that zone:

# create a list of all raw log files to be processed
from path import path
month = path("/gpslogs/200808")
logs = " ".join(["-i nmea -f %s"%log 
                 for log in sorted((month/"raw").files("GPS_*.log"))])

for (id,(place,lat,lon,radius)) in places.items():
   os.system("gpsbabel "
             # input files
             + logs
             # convert to waypoints
             + " -x transform,wpt=trk,del"
             # remove anything outside place of interest
             + (" -x radius,distance=%.3fK,lat=%.6f,lon=%.6f,nosort"%(radius,lat,lon))
             # convert back to tracks
             + " -x transform,trk=wpt,del"
             # output nmea to stdout
             + " -o nmea -F -"
             # filter to just GPRMC sentences
             + " | grep GPRMC"
             # output to log file
             + (" > %s/processed/place%d.log"%(month,id)))

And all points outside any of the specific places of interest are sent into an "other" file:

os.system("gpsbabel "
          # input files
          + logs
          # convert to waypoints
          + " -x transform,wpt=trk,del"
          # remove anything in a place of interest
          + "".join([" -x radius,distance=%.3fK,lat=%.6f,lon=%.6f,nosort,exclude"%(radius,lat,lon)
                     for (id,(place,lat,lon,radius)) in places.items()])
          # convert back to tracks
          + " -x transform,trk=wpt,del"
          # output nmea to stdout
          + " -o nmea -F -"
          # filter to just GPRMC sentences
          + " | grep GPRMC"
          # output to log file
          + (" > %s/processed/place%d.log" % (month, otherid)))

These files are filtered with grep to contain only minimal data as we only require the timestamps for this part of the process. Specifically only the NMEA GPRMC sentences are kept.

To provide a brief illustration, the following picture shows two log files of data, a blue and a green, between three points of interest:

The above process would create four files, one for each point A, B and C and one for "Other" points that would contain something like the following information, where the horizontal axis represents time:

I then read all those log files back in to create a "time line" that for each timestamp stores my "location" in the sense that it knows whether I was "home", at "work" or somewhere between the two.

# dict of timestamp (seconds since epoch, UTC) to placeid
where = {}
for placeid in places.keys()+[otherid,]:
   for line in (month/"processed"/("place%d.log"%placeid)).lines():
      fields = line.split(",")
      # convert date/time to seconds since epoch (UTC)
      t, d = fields[1], fields[-3]
      ts = calendar.timegm( (2000+int(d[4:6]), int(d[2:4]), int(d[0:2]),
                             int(t[0:2]), int(t[2:4]), int(t[4:6])) )
      where[ts] = placeid

This is then summarised from one value per second to a list of "segments" with a start and end time and a location. Unlogged time segments are also inserted at this point whenever there are no logged readings for 5 minutes or more.

# array of tuples (placeid, start, end, logged)
# placeid = 0 indicates "unknown location", i.e. unlogged
summary = []
current, start, stop, last_ts = 0, 0, 0, None
for ts in sorted(where.keys()):
   # detect and insert "gaps" if space between logged timestamps is greater than 5 minutes
   if last_ts and ts-last_ts > 5*60:
      if current:
         summary.append( [current, start, stop, True] )
      current, start, stop = where[ts], ts, ts
      summary.append( [0, last_ts, ts, False] )
 
   last_ts = ts

   if where[ts] != current:
      if current:
         summary.append( [current, start, stop, True] )
      current, start, stop = where[ts], ts, ts
   else:
      stop = ts
summary.append( [current, start, stop, True] )

(If there's a more "Pythonic" way of writing that kind of code, I'd be interested in knowing it.)

"Spurious" segments are then removed. These show up because when the logger is inside buildings the location jumps around and often out of the 50m radius meaning that, for example, there will be a sequence of Home-Other-Home-Other-Home logs. The "Other" segments that are between two known points of interest and less than 5 minutes long are deleted, as are "Other" segments that sit between a known place of interest and an unlogged segment.

Based on the above graphic, the summary might look something like the following:

startendlocation
10.00am10.05amA
10.05am10.30amOther
10.30am10.35amB
10.35am11.00amOther
...

The "Other" segments are then labelled if possible to indicate they were "commutes" between known locations:

startendlocation
10.00am10.05amA
10.05am10.30amA-B
10.30am10.35amB
10.35am11.00amB-C
...

Some segments cannot be labeled automatically and are left as "Other". This may be a trip out to a "one-off" location and back again, which can be left as "Other". However, sometimes it is because the logger didn't lock onto the satellites within the 50m radius on the way out of a place of interest and these can be manually fixed up later.

Once a list of "activities" has been obtained, with start and end times, it is easy to use gpsbabel again to split logs based on start and end of time segments:

for (place, start, stop, place_from, place_to, logged) in summary:
    dest = month / "processed" / ("%s-%s"%(time.strftime("%Y%m%d%H%M%S", time.localtime(start)),
                                           time.strftime("%Y%m%d%H%M%S", time.localtime(stop))))

   for (ext, chopFn) in [(".log", chopToLog),
                         (".kml", chopToKml), 
                         (".speed", chopToSpeedVsDistance), 
                         (".alt", chopToAltitudeVsDistance), 
                         (".hist", chopToSpeedHistogram),
                         (".head", chopToHeadingHistogram),
                         (".stops", chopToStopsVsDistance)]:
      if not (dest+ext).exists():
         chopFn(dest, locals())
         # make the file in case it was empty and not created
         (dest+ext).touch()

This generates a bunch of files for each segment, named with the start and end timestamps of the segment and an extension depending on the content. The first "chop" function generates an NMEA format log file that is then processed further by the remaining "chop" functions. The other chop functions will probably be explained in a later post, the first two are:

def chopToLog(dest, p):
    # filter input file entries within times of interest to temp file
    os.system("gpsbabel " + p["logs"]
              + (" -x track,merge,start=%s,stop=%s"
                 % (time.strftime("%Y%m%d%H%M%S", time.gmtime(p["start"])),
                    time.strftime("%Y%m%d%H%M%S", time.gmtime(p["stop"]))))
              + " -o nmea -F "+sh_escape(dest)+".log")

def chopToKml(dest, p):
    # create kml file with reduced resolution
    os.system("gpsbabel -i nmea -f "+sh_escape(dest)+".log"
              + " -x simplify,error=0.01k"
              + " -o kml -F "+sh_escape(dest)+".kml")

def sh_escape(p):
    return p.replace("(","\\(").replace(")","\\)").replace(" ","\\ ")

(Again, if there's a better way to handle escaping special characters in shell commands, I would like to know it.)

Using this, I can simply plug in the logger, which launches an autorun script, and the end result are nicely segmented log files that I can map and graph. More about that in another post.

Sunday, May 18, 2008

Ambient Email Notifier in Linux

I finally got around to hooking up my Ambient Email Notifier under Ubuntu. No idea why I waited so long, given it pretty much worked as soon as I plugged it in!

The Linux kernel has drivers for the CP2102 USB-to-RS232 chip built in, so as soon as it was plugged in it showed up as /dev/ttyUSB0.

The Python-Serial module is in the Ubuntu repositories, so after installing that, I just changed my checkinbox.py script to use /dev/ttyUSB0 instead of COM3 and added a cron job to check every 10 minutes.

Friday, February 29, 2008

Ambient Email Notifier (some code)

Will asked about the code I was using in my ambient email notifier. The full code is a bit difficult to figure out because I've got it tied to a system tray icon thingi, which can go in another post another day, but here are some relevant bits.

First, you can get the number of emails with a particular label in gmail with the following Python code:

import feedparser
def msgCount(uid, pwd, filter):
    inbox = feedparser.parse("https://%s:%s@gmail.google.com/gmail/feed/atom%s" % (uid, pwd, filter))
    return len(inbox["entries"])

uid is your gmail address without the @gmail.com bit, filter is "" for the inbox, and "/label/" to get messages tagged with a particular label.

So, after calling msgCount a few times, for different labels, I compute the colour of the RGB LED:

colour = (inbox > 0 and 1 or 0) + (news > 0 and 2 or 0) + (work > 0 and 4 or 0)

This is sent over the serial port to the picaxe which decodes bit 0 for blue, 1 for green and 2 for red.

def triggerAmbient(colour):
    com = serial.Serial("COM3", 2400, timeout=0.25)
    for attempt in range(0,10):
        com.write("%c" % (colour) )
    com.close()

It tries to send a few times in case the picaxe doesn't get it the first time. The code on the picaxe just listens for a byte on the serial input and outputs the lowest 4 bits to the output pins:

main:
 serout 0, n2400, ("Ok")
 serin 3, n2400, b0
 gosub nibble3
 b0 = b0 & 7
 serout 0, n2400, (#b0)
 goto main
 
nibble3:
 if bit2 = 1 then
  high 1
 else
  low 1
 endif
 if bit1 = 1 then
  high 2
 else
  low 2
 endif
 if bit0 = 1 then
  high 4
 else
  low 4
 endif
 return 

This means the python script has control over the colour and you can test that it's working by simply opening up a terminal on COM3 and typing away (A is 01000001 in ASCII, meaning pin 1 is switched on, B is 01000010 so pin 2 is on, etc.)

Update: I've detailed the changes I made to get this working under Linux.

Friday, January 18, 2008

Cover-art Wallpaper under Ubuntu

This is an update to the previous post to create a cover art "stack" for Windows XP wallpaper to generate and update the wallpaper in Ubuntu.

Not much had to change, except the setWallpaper function:

def setWallpaper( bmp ):
    os.system( "gconftool-2 -t string -s /desktop/gnome/background/picture_filename %s" % bmp.replace(" ","\\ ") )

Monday, November 19, 2007

Now Playing Album Cover Art Desktop Wallpaper

A little Python program I wrote to update my Windows desktop wallpaper with the album cover art of each album I play. The idea is to represent a stack of CDs with the latest played one on top. This concept could easily be extended to other images, a random photo each day springs to mind.

The details of how this script is run when an album is played and where the album cover art comes from aren't really relevant, and aren't going to work outside my system anyway, but the concept can be used easily enough.

Windows requires a little messing about to change the wallpaper dynamically, and this will only work with BMP files:

def setWallpaper( bmp ):
   import win32api, win32con, win32gui
   k = win32api.RegOpenKeyEx(win32con.HKEY_CURRENT_USER,"Control
Panel\\Desktop",0,win32con.KEY_SET_VALUE)
   win32api.RegSetValueEx(k, "WallpaperStyle", 0, win32con.REG_SZ, "0")
   win32api.RegSetValueEx(k, "TileWallpaper", 0, win32con.REG_SZ, "0")
   win32gui.SystemParametersInfo(win32con.SPI_SETDESKWALLPAPER, bmp, 1+2)

The following functions generate the wallpaper. The main function takes the filename of the wallpaper file, and the name of the new cover image, which will be pasted over the top of the existing wallpaper.

# read bmp in old_filename, "paste" current_filename over the top 
# and write out to new_filename (usually the same as old_filename)
import sys, os, urllib, math, Image, random
def makeBackgroundCollage(old_filename, current_filename, new_filename):
   if os.path.exists( old_filename ):
      new = Image.open( old_filename )
   else:
      new = Image.new( "RGB", (1280, 1024), (58, 110, 165) )
   if os.path.exists( current_filename ):
      current = Image.open( current_filename)
      if max(current.size) < 300:
         current = current.resize( (current.size[0]*2, current.size[1]*2) )
      if max(current.size) > 600:
         current = current.resize( (600, 600 * current.size[1]/current.size[0]) )
      border = Image.new("RGB", (current.size[0]+10, current.size[1]+10), 0xffffff)
      shadow = Image.new("RGB", border.size, 0x000000)
      angle = int(random.betavariate(2,2) * 50 - 25)
      (shadow, shadow_mask) = rotate2(shadow, border.size, angle, 128)
      (current, current_mask) = rotate2(current, border.size, angle)
      (border, border_mask) = rotate2(border, border.size, angle)
      pos = (int(random.betavariate(2,2) * (new.size[0] - border.size[0])),
             int(random.betavariate(2,2) * (new.size[1] - border.size[1])))
      new.paste(shadow, (pos[0]+5, pos[1]+5), shadow_mask)
      new.paste(border, pos, border_mask)
      new.paste(current, pos, current_mask)
   new.save( new_filename )

def rotate2(img, box, angle, alpha=255):
   img2 = Image.new(img.mode, boundingBox(box, angle * math.pi/180.0), 0)
   img2.paste(img, ( img2.size[0]/2 - img.size[0]/2,
                     img2.size[1]/2 - img.size[1]/2 ) )
   mask2 = Image.new("L", img2.size, 0)
   mask2.paste(Image.new("L", img.size, alpha), ( img2.size[0]/2 - img.size[0]/2,
                                                  img2.size[1]/2 - img.size[1]/2 ) )
   return (img2.rotate( angle, Image.BICUBIC ),
           mask2.rotate( angle, Image.BICUBIC ))

def boundingBox(box, angle):
   (x,y) = (box[0]/2.0, box[1]/2.0)
   (r, a) = (math.sqrt(x*x+y*y), math.atan2(y, x))
   (x1, y1) = (r * math.cos( a+angle ), r * math.sin( a+angle ) )
   (x2, y2) = (r * math.cos( -a+angle ), r * math.sin( -a+angle ) )
   return (int(math.ceil(max(abs(x1),abs(x2))*2)),
           int(math.ceil(max(abs(y1),abs(y2))*2)))

The position and rotation of the album is chosen using a beta distribution. This gives a nice distribution weighted towards the center of the screen, borders and a "drop shadow" are added to enhance the "stack" effect.

Friday, July 06, 2007

Ambient Email Notifier

Small project to provide email notification via an RGB LED hooked up to the USB port.

Uses a 4d-micro-USB module to provide power and RS232 to a Picaxe 08M controlling an RGB LED. Can control colour (seven colours: work email in red, newsletters in green, other in blue) and brightness by sending characters to the COM port, this is done by a Python script run every 10 minutes by Task Scheduler with an accompanying system tray icon to allow LED to be turned off again (see python gmail check in four lines of code, pySerial and pySystray).

The schematic is as simple as possible:

The relevant code (python and picaxe basic) is pretty self-evident, I will provide it if anyone is interested. Update: Another post has some code.