最近は仕事で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局程しか交信できていませんでした。 もう少しアクティブに運用しないといけませんね。