Contest Log Analysis

So - can we step back from the Contest logger and try and see the bigger picture ? Yes.

Data Sources

To start with I will assume that you only have the following

  • Dx Cluster/Skimmer data file

99% of all contests need this, and if you are using pen and paper - or Excel... well it's the 21st centuary - get a contest logger - and move with the times.

The data processing tools we will use are

  • Open Source
  • Free
  • Work on just about every Software platform

Analysis Software

You will need to install the following

  • Python3

I strongly suggest that you install python using a venv mechanism (please Google how to do this).

Python 3 Modules

As a bare minimum you will need to add the following modules into your venv Python3 environment.

  • Numpy
  • Pandas
  • Matplotlib
  • Jupyter

To set up an env from scratch (Linux/Mac) it is this

cd 
python3 -m venv ~/.pecontest
source ~/.pecontest/bin/activate
# We now are using our private python instalation
# So no need for sudo/root commands
pip3 install numpy pandas matplotlib jupyter 
# That is it

Load the data

So with a python environment setup, we start like this

cd 
python3 -m venv ~/.pecontest
source ~/.pecontest/bin/activate
mkdir ~/contest-data
cp <Your Cabrillo File> ~/contest-data/<contest_name>
cd ~/contest-data
jupyter notebook 

We now should have a Jupter notebook loaded.

This is what my datafile look like (this is Skimmer format)

2020-09-26 01:55:36Z   14085.8  VR2CC       26-Sep-2020 0155Z   23 dB  45 BPS  DE RTTY       <DU3TW-#>
2020-09-26 01:55:38Z   14090.8  YB8UTI      26-Sep-2020 0155Z   21 dB  45 BPS  DE RTTY       <DU3TW-#>
2020-09-26 01:55:44Z   14085.8  VR2CC       26-Sep-2020 0155Z   22 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:55:52Z   21090.7  JA4XHF/3    26-Sep-2020 0155Z   18 dB  45 BPS     RTTY       <DU3TW-#>
2020-09-26 01:55:53Z   14090.8  YB8UTI      26-Sep-2020 0155Z   17 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:55:55Z   21098.1  YB2MM       26-Sep-2020 0155Z   12 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:55:55Z   21098.7  YB2MM       26-Sep-2020 0155Z   20 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:55:55Z   21100.0  YB2MM       26-Sep-2020 0155Z   11 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:55:55Z   21098.1  YB2MM       26-Sep-2020 0155Z   12 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:55:55Z   21098.7  YB2MM       26-Sep-2020 0155Z   20 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:55:56Z   21100.0  YB2MM       26-Sep-2020 0155Z   11 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:55:59Z   14083.9  YC1WCK      26-Sep-2020 0155Z   21 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:56:02Z   14083.3  JA2FSM      26-Sep-2020 0156Z   18 dB  45 BPS  DE RTTY       <DU3TW-#>
2020-09-26 01:56:02Z   14084.9  JA2FSM      26-Sep-2020 0156Z   19 dB  45 BPS  DE RTTY       <DU3TW-#>
2020-09-26 01:56:04Z   14083.3  JA2FSM      26-Sep-2020 0156Z   18 dB  45 BPS  DE RTTY       <DU3TW-#>
2020-09-26 01:56:04Z   14084.9  JA2FSM      26-Sep-2020 0156Z   19 dB  45 BPS  DE RTTY       <DU3TW-#>
2020-09-26 01:56:08Z   14089.8  JA1RRA      26-Sep-2020 0156Z   14 dB  45 BPS     RTTY       <DU3TW-#>
2020-09-26 01:56:11Z   21098.1  YB2MM       26-Sep-2020 0156Z   11 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:56:11Z   21098.7  YB2MM       26-Sep-2020 0156Z   19 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:56:12Z   21098.1  YB2MM       26-Sep-2020 0156Z   11 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:56:12Z   21098.7  YB2MM       26-Sep-2020 0156Z   19 dB  45 BPS  CQ RTTY       <DU3TW-#>
2020-09-26 01:56:16Z   14088.9  VU2DED      26-Sep-2020 0156Z   22 dB  45 BPS     RTTY       <DU3TW-#>
2020-09-26 01:56:19Z   14084.0  JA2FSM      26-Sep-2020 0156Z   27 dB  45 BPS     RTTY       <DU3TW-#>

Preparing the data

This first step is making this Skimmer/Telnet cluster data file into a standard pandas Dataframe.

This is using a set of python modules that I wrote (ham.dxcc) available on my github page .

import pandas as pd
from ham.dxcc import DxccAll
from ham.band import HamBand
import pickle


dx=DxccAll()

with open("cqwwrtty2020.txt","rt") as infile:
    data=infile.read().split('\n')


from dataclasses import dataclass
@dataclass
class RttySkimmer:
    when:str
    freq:float
    call:str
    when2:str
    sigdb:int
    baudrate:str
    cqde: str
    mode: str


class RttySkimmerLoader:
    '''2020-09-26 01:55:36Z   14085.8  VR2CC       26-Sep-2020 0155Z   23 dB  45 BPS  DE RTTY       <DU3TW-#>'''
    '''                                                                                                    1'''
    '''          1         2         3         4         5         6         7         8         9         0'''
    '''01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'''
    def __init__(self, list_of_lines):
        self.obj=[]
        self.data=[]
        fields=[(0,20),
                 (22,30),
                 (32,42),
                 (43,62),
                 (63,66),
                 (70,73),
                 (78,81),
                 (82,90)]
        for n in list_of_lines:
            parts = [n[f[0]:f[1]].strip() for f in fields] 
            self.data.append(parts)
            self.obj.append(RttySkimmer(*parts))

    def names(self):
        return ['when','freq','call','when2','sigdb','baudrate','cqde','mode']

    def get_obj(self):
        return self.obj

    def get_data(self):
        return self.data


rtty = RttySkimmerLoader(data)   # Chop the file into RttySkimmer objects
#rtty_data_list=rtty.get_obj()  # Get these objects back
contest=pd.DataFrame.from_records(rtty.get_data())
contest.columns=rtty.names()
#
# Change some data types
# This could be done in the importer function....
contest['freq'] = contest.freq.apply(lambda x: float('0'+x))
contest['sigdb'] = contest.sigdb.apply(lambda x: int('0'+x))
contest['baudrate'] = contest.baudrate.apply(lambda x: int('0'+x))
contest['when'] = contest.when.apply(lambda x: pd.to_datetime(x))
contest['when2'] = contest.when2.apply(lambda x: pd.to_datetime(x))
# Check the top of the Dataframe
contest.head()

# Add some extra fields
#

def get_country(call):
    fnd = dx.find(call)
    if fnd and fnd.Country_Name:
        return fnd.Country_Name
    else:
        return ""

def get_cqzone(call):
    fnd = dx.find(call)
    if fnd and fnd.CQ_Zone:
        return fnd.CQ_Zone
    else:
        return 0


def get_lat(call):
    fnd = dx.find(call)
    if fnd and fnd.Latitude:
        return fnd.Latitude
    else:
        return 0


def get_lon(call):
    fnd = dx.find(call)
    if fnd and fnd.Longitude:
        return -1*fnd.Longitude
    else:
        return 0

def get_continent(call):
    fnd = dx.find(call)
    if fnd and fnd.Continent_Abbreviation:
        return fnd.Continent_Abbreviation
    else:
        return ""

def get_time_hour(when):
    """ Round the time to the nearest hour"""
    return when.round('h').hour   

band = HamBand()
# Fields I create have upper Case Name. So I can tell if it is source or Inferred data.
print("Calculating Country")
contest['Country']=contest.call.apply(lambda x: get_country(x))
print("Calculating Band")
contest['Band']=contest.freq.apply(lambda x: band.khz_to_m(x))
print("Calculating CQZone")
contest['CQZone']=contest.call.apply(lambda x: get_cqzone(x))

print("Calculating Lat")
contest['Lat']=contest.call.apply(lambda x: get_lat(x))
print("Calculating Lon")
contest['Lon']=contest.call.apply(lambda x: get_lon(x))
print("Calculating Continent")
contest['Cont']=contest.call.apply(lambda x: get_continent(x))
print("Calculating Rounded Hour")
contest['ZHour']=contest.when.apply(lambda x: get_time_hour(x))

print("Saving Data")
with open('contest.pkl',"wb") as ofp:
    pickle.dump(contest,ofp)
print("Done")

We now have a pickle file - which has a constant format

when freq call when2 sigdb baudrate cqde mode Country Band CQZone Lat Lon Cont ZHour
0 2020-09-26 01:55:36+00:00 14085.8 VR2CC 2020-09-26 01:55:00+00:00 23 45 DE RTTY Hong Kong 20.0 24 22.28 114.18 AS 2.0
1 2020-09-26 01:55:38+00:00 14090.8 YB8UTI 2020-09-26 01:55:00+00:00 21 45 DE RTTY Indonesia 20.0 28 -7.30 109.88 OC 2.0
2 2020-09-26 01:55:44+00:00 14085.8 VR2CC 2020-09-26 01:55:00+00:00 22 45 CQ RTTY Hong Kong 20.0 24 22.28 114.18 AS 2.0
3 2020-09-26 01:55:52+00:00 21090.7 JA4XHF/3 2020-09-26 01:55:00+00:00 18 45 RTTY Japan 15.0 25 36.40 138.38 AS 2.0
4 2020-09-26 01:55:53+00:00 14090.8 YB8UTI 2020-09-26 01:55:00+00:00 17 45 CQ RTTY Indonesia 20.0 28 -7.30 109.88 OC 2.0

and has some extra columns - such as

  • sigdb
  • Baudrate (Rtty Contest)
  • cqde
  • Country
  • Band
  • CQ Zone
  • Their Lat/Lon
  • Continent

At this point we are ready to start looking into the data, which will be covered in part 2 of log analysis but as this will be using some geo data we need to add some more files

Prerequisites

This is for a Mac... (assuming you are using your pecontest version of python).

brew install gdal
pip install pyproj==1.9.6
pip install  geopandas seaborn