A1CCをPandasで集計してみる

最近は仕事でPythonを使っています。はじめは例の「インデント」が気持ち悪く感じていたのですが、 次第に違和感もなくなり、プログラミングを楽しめるようになってきました。

Pythonは便利なライブラリがとても多いのが魅力ですね。今回はその中でもデータ解析を簡単に行えるPandasを使って A1CCの申告データを集計してみたいと思います。

A1CCとは

A1CCはA1 CLUBが主催するランキングです。A1 CLUBメンバーの中で交信したことのあるメンバーの数を競います。

A1CCには次のカテゴリがあります。

  • Mixed: モードは問わず交信したことのあるメンバーの数
  • CW: モールス通信で交信したことのあるメンバーの数
  • Single Band: CWで交信したことのあるメンバーの数を各バンド毎に求めたもの
  • Challenge: Single Bandの各バンドの合計

A1CCの集計に使うデータ

A1CCの集計には次の二つのデータを使用します。

  • A1 CLUB会員情報
  • 交信データ

A1 CLUB会員情報

A1 CLUB会員情報には、A1 CLUBのウェブサイトで一般にも公開されている次のデータを利用します。

https://a1club.org/roster/mbr_nr.txt

このデータは次のように会員ナンバーとコールサインの組になっています。

As of 2022/02/12
#           CALL
      0X    JO1ZZZ
       1    JS2AHG
       1    KH0ZZ
     1M2    JE1TRV
       2    JH2TWJ
       3    JE1LGY

この記事では説明のために、次のような架空のコールサインが記載されているデータを使います。

#           CALL
       0    JA1QRA
       1    JA1QRB
       2    JA1QRC
       ...(省略)...
      23    JA1QRX
      24    JA1QRY
      25    JA1QRZ

交信データ

交信データにはユーザー数の多いハムログのデータを使うことにします。 ハムログからCSVファイルをエクスポートとすると、次のような形式で出力されます。

[Call],[Date],[Time],[His],[My],[Freq],[Mode],[Code],[GL],[QSL],[Name],[QTH],[Remarks1],[Remarks2],[DC12],[hQSL]

こちらも説明のために、架空のコールサインが記載されているデータを作って使います。

A1 CLUB会員情報の読み込み

A1 CLUB会員情報の読み込みには read_csv を使用します。

def load_roster(f):
    df = pd.read_csv(f, header=1, delim_whitespace=True, names=('#', 'callsign'))
    return df

会員情報ファイルの1行目は日付ですので header=1 でスキップします。各列はスペースで区切られているので delim_whitespace=True とします。 また、後続の処理のためにコールサインの列名は callsign に変更しておきます。

読み込んだDataFrameを表示してみると次のようになります。

     # callsign
0    0   JA1QRA
1    1   JA1QRB
2    2   JA1QRC
...(省略)...

ハムログデータの読み込み

ハムログデータの読み込みも同じように read_csv を使用します。

def load_hamlog_csv(f):
    df = pd.read_csv(f, header=None, dtype=str,
                     names=('Call', 'Date', 'Time', 'His', 'My', 'Freq', 'Mode',
                            'Code', 'GL', 'QSL', 'Name', 'QTH',
                            'Remarks1', 'Remarks2', 'DC12', 'hQSL'),
                     usecols=['Call', 'Freq', 'Mode'])
    return df

ヘッダーはないので header=None にします。usecols で抽出する列を指定しています。ここでは集計に必要な Call/Freq/Mode を抽出しています。

次のようなCSVファイルを読み込んでみます。

JA1QRA,22/01/01,00:00J,599,599,7.025,CW,,,,,,,,,
JA1QRB,22/01/01,00:00J,59,59,144.150,SSB,,,,,,,,,
JA1QRC,22/01/01,00:00J,59,59,433.020,FM,,,,,,,,,

DataFrameは次のようになります。

     Call     Freq Mode
0  JA1QRA    7.025   CW
1  JA1QRB  144.150  SSB
2  JA1QRC  433.020   FM

コールサインの正規化

ポータブル表記されたコールサインから正式なコールサインの部分を抜き出します。

def normalize_callsign(callsign):
    return sorted(callsign.split('/'), key=len, reverse=True)[0]

ここでは仮に '/' で文字列を分割して、文字列長が最大のものをコールサインとみなしました。

これを用いて交信データの各行の交信局 'Call' のコールサインを正規化して、新しい列 callsign に追加します。

    df_hamlog['callsign'] = df_hamlog['Call'].map(lambda x: normalize_callsign(x))

次のような交信データのコールサインを正規化してみます。

       Call     Freq Mode
0    JA1QRA    7.025   CW
1    JA1QRB  144.077   CW
2  JA1QRB/1  144.150  SSB
3  JA1QRC/2  433.020   FM

次のように正規化されたコールサインが callsign 列に追加されます。

       Call     Freq Mode callsign
0    JA1QRA    7.025   CW   JA1QRA
1    JA1QRB  144.077   CW   JA1QRB
2  JA1QRB/1  144.150  SSB   JA1QRB
3  JA1QRC/2  433.020   FM   JA1QRC

周波数からメーターバンドに変換

A1CCのカテゴリSingle Bandはバンド毎に集計するため、交信データの周波数をメーターバンドに変換します。 周波数の欄には 7.025 という表記だけでなく、7 のような表記もあるので、どちらも変換できるようにします。 判定方法は次のように手を抜いています。

def freq2meter_band(f):
    if re.match('5\d{3}', f):  return '6cm'
    if re.match('24\d{2}', f): return '13cm'
    if re.match('12\d{2}', f): return '23cm'
    if re.match('43\d{1}', f): return '70cm'
    if re.match('14\d{1}', f): return '2m'
    if re.match('5\d{1}', f):  return '6m'
    if re.match('2[89]', f):   return '10m'
    if re.match('24', f):      return '12m'
    if re.match('21', f):      return '15m'
    if re.match('18', f):      return '17m'
    if re.match('14', f):      return '20m'
    if re.match('10', f):      return '30m'
    if re.match('7', f):       return '40m'
    if re.match('3\.', f):     return '80m'
    if re.match('1\.', f):     return '160m'
    return "?"

これを交信データの各行の周波数 'Freq' に適用して、新しい列 meter_band に追加します。

    df_hamlog['meter_band'] = df_hamlog['Freq'].map(lambda x: freq2meters(x))

次のような交信データを変換してみます。

     Call     Freq Mode callsign
0  JA1QRA    7.025   CW   JA1QRA
1  JA1QRB  144.077   CW   JA1QRB
2  JA1QRC  433.020   FM   JA1QRC

次のように meter_band 列が追加されます。

     Call     Freq Mode callsign meter_band
0  JA1QRA    7.025   CW   JA1QRA        40m
1  JA1QRB  144.077   CW   JA1QRB         2m
2  JA1QRC  433.020   FM   JA1QRC       70cm

モードの正規化

モールス通信の場合はモードに CW という表記以外に A1A/A2A/F2A と記載されているケースもあります。 これらをすべて CW にします。

def normalize_mode(m):
    return 'CW' if m in ['CW', 'A1A', 'A2A', 'F2A'] else m

これを交信データの各行のモード 'Mode' に適用して、新しい列 mode に追加します。

    df_hamlog['mode'] = df_hamlog['Mode'].map(lambda x: normalize_mode(x))

次のような交信データを変換してみます。

     Call     Freq Mode callsign meter_band
0  JA1QRA    7.025   CW   JA1QRA        40m
1  JA1QRB  144.077  A1A   JA1QRB         2m
2  JA1QRC  433.020  A2A   JA1QRC       70cm
3  JA1QRD  433.120  F2A   JA1QRD       70cm

次のように mode 列が追加されます。

     Call     Freq Mode callsign meter_band mode
0  JA1QRA    7.025   CW   JA1QRA        40m   CW
1  JA1QRB  144.077  A1A   JA1QRB         2m   CW
2  JA1QRC  433.020  A2A   JA1QRC       70cm   CW
3  JA1QRD  433.120  F2A   JA1QRD       70cm   CW

A1 CLUBメンバーとの交信の抽出

全交信データからA1 CLUBメンバーとの交信を抽出します。 これは全交信データとA1 CLUBのメンバーデータとをマージ(内部結合)することで抽出できます。

    df_a1c_qsos = pd.merge(df_hamlog, df_roster, on=['callsign'])

次のような交信データを変換してみます。 ここではサフィックスQR* のコールサインはメンバー、QS* のコールサインはメンバーではないコールサインとします。

     Call   Freq Mode callsign meter_band mode
0  JA1QRA  7.025   CW   JA1QRA        40m   CW
1  JA1QSA  7.025   CW   JA1QSA        40m   CW
2  JA1QRB  7.025   CW   JA1QRB        40m   CW
3  JA1QSB  7.025   CW   JA1QSB        40m   CW
4  JA1QRC  7.025   CW   JA1QRC        40m   CW
5  JA1QSC  7.025   CW   JA1QSC        40m   CW

次のように A1 CLUBのメンバーのみ抽出されます。

     Call   Freq Mode callsign meter_band mode  a1club#
0  JA1QRA  7.025   CW   JA1QRA        40m   CW        0
1  JA1QRB  7.025   CW   JA1QRB        40m   CW        1
2  JA1QRC  7.025   CW   JA1QRC        40m   CW        2

A1 CLUBメンバーとのCW交信の抽出

A1 CLUBメンバーとのCW交信の抽出は、先程求めたA1 CLUBメンバーとの交信のうち、mode 列が CW のものを抽出します。

    df_a1c_cw_qsos = df_a1c_qsos[df_a1c_qsos['mode'] == 'CW']

A1CCの集計

ここまででA1CCを集計する準備が整いましたので、各カテゴリの得点を集計していきます。

「Mixed」は、A1 CLUBメンバーとの全交信データからユニークなコールサインの数をカウントします。

    mixed = df_a1c_qsos['callsign'].nunique()

「CW」は、A1 CLUBメンバーとのCW交信データからユニークなコールサインの数をカウントします。

    cw = df_a1c_cw_qsos['callsign'].nunique()

「Single Band」は、A1 CLUBメンバーとのCW交信データの各バンド毎にユニークなコールサインの数をカウントします。

    single_band = df_a1c_cw_qsos.groupby('meter_band').agg({'callsign' : 'nunique'})

「Challenge」は、各バンド毎に求めたコールサインの数を合計したものです。

    challenge = single_band['callsign'].sum()

まとめ

Pandasを使うことでとても簡単にA1CCを求めることができました。

実際の交信データで試してみたところ、まだ300局程しか交信できていませんでした。 もう少しアクティブに運用しないといけませんね。