ふらっと巡回していたら
https://qiita.com/rana_kualu/items/cd65cd6a05ac62c9a3e8
手頃な問題があったので記事を読む前に自分でやってみる。
https://www.yumemi.co.jp/serverside_recruit
CLIでCSV入力から平均値を計算してCSV出力する問題。
create_timestamp,player_id,score
2021/01/01 12:00,player0001,12345
2021/01/02 13:00,player0002,10000
2021/01/03 12:00,player0021,100
2021/01/04 12:10,player0031,200
2021/01/05 12:00,player0041,300
プレイヤーは1万人以下、入力行数は数千万行になるというところがポイントっぽい。
特に何も考えずに上から順にベタ書きしていく。
<?php
if (!isset($argv[1])){
echo "Usage:
\tget_ranking.php <csv file>
";
exit(1);
}
if (!file_exists($argv[1])){
echo "
error: [$argv[1]] is not exists.
";
}
$fp = fopen($argv[1], 'r');
$players = [];
fgetcsv($fp);
while ($line = fgetcsv($fp)){
if (!isset($players[$line[1]]))
$players[$line[1]] = [];
$players[$line[1]][] = (int)$line[2];
}
fclose($fp);
$avg = [];
foreach ($players as $player => $scores){
$avg[$player] = round(array_sum($scores) / count($scores));
}
arsort($avg);
$fp = fopen('php://stdout', 'w');
fputcsv($fp, ['rank', 'player_id', 'mean_score']);
$i = 0;
$rank = 0;
$prev = null;
foreach ($avg as $player => $score){
$i++;
if ($prev != $score){
$rank = $i;
}
if ($rank > 10)
break;
fputcsv($fp, [$rank, $player, $score]);
$prev = $score;
}
普段はstream_filter_register
を挟んでfopen
するユーティリティ的な関数を使うところだけど、SJISを考えなくていいのでそのままfopen。
配列に溜めて計算。
ここまで30分ぐらい?
参考資料は、php.net/sort とブラウザに打ち込んでソート関数のマニュアルを見たのと、array_average
的な関数があるんじゃね?と思って探したけど無かった。
<?php
$fp = fopen('php://stdout', 'w');
for ($i = 0; $i < 10000000; $i++){
fputcsv($fp, ['2021/01/01 12:00', 'player'.rand(1,9999), rand(100, 1000)]);
}
データを生成するスクリプトを書いて1000万件をテストしてみると、速度は21sだがメモリ使用が 727MB・・・。こりゃあかん。
@@ -18,14 +18,15 @@ $players = [];
fgetcsv($fp);
while ($line = fgetcsv($fp)){
if (!isset($players[$line[1]]))
- $players[$line[1]] = [];
- $players[$line[1]][] = (int)$line[2];
+ $players[$line[1]] = [0, 0];
+ $players[$line[1]][0] += (int)$line[2];
+ $players[$line[1]][1]++;
}
fclose($fp);
$avg = [];
-foreach ($players as $player => $scores){
- $avg[$player] = round(array_sum($scores) / count($scores));
+foreach ($players as $player => [$scores, $count]){
+ $avg[$player] = round($scores / $count);
}
配列に溜めてからの array_sum
がメモリ大食いな気がするので、合計は手動に変更。
メモリ5.9MB、速度21sになった。
よく見たら、答えが間違ってる!?
後で気付いたけど、ちゃんとテストできるようになってるのね。
https://github.com/yumemi-inc/serverside-engineer-codecheck-practice
データ生成のスクリプトもあったね。
順位が同じ場合のソートが抜けていたので変更する。
$fp = fopen($argv[1], 'r');
$players = [];
fgetcsv($fp);
while ($line = fgetcsv($fp)){
if (!isset($players[$line[1]]))
$players[$line[1]] = [0, 0];
$players[$line[1]][0] += (int)$line[2];
$players[$line[1]][1]++;
}
fclose($fp);
$data = [];
foreach ($players as $player => [$scores, $count]){
$avg = round($scores / $count);
if (!isset($data[$avg]))
$data[$avg] = [];
$data[$avg][] = $player;
}
krsort($data);
$fp = fopen('php://stdout', 'w');
fputcsv($fp, ['rank', 'player_id', 'mean_score']);
$i = 1;
foreach ($data as $score => $players){
if ($i > 10)
break;
$rank = $i;
sort($players);
foreach ($players as $player){
fputcsv($fp, [$rank, $player, $score]);
$i++;
}
}
何か良い方法がないかと思って考えたけども、特に思い付かなかったのでPHPerらしく配列で。
5.5MB、21s になった。
ここまでで45分ぐらいか。
うーん、見返すと何が何やら分からんね。業務でもcsvが100列以上あったりしたら(列番号をconstで定義したり関数を細かく分けたりしても)何が何だか分からなくなりがち。PDFとか。
もしこのコードを綺麗に書くとしても、全体を関数に包むぐらいで分割はしないかな。
一応それぞれのループ処理が分割できるっぽく分かれているけれども、CSVからデータを読み込むパーツも、平均を計算するパーツも、出力するパーツも、データ構造を変えたら他のループを変える必要があるので分けられない。
インターフェースを使って分離できるようにするか?といってもクラスを使うとちょっと重くなりそうなので配列のままがいいと思う。
もしもっと変更管理しやすくするとしたらDBに入れてrank()
でやるとかマテビューを使うとか、KVSを使うとかクラウドでごにょごにょするとか、ガラッと変わってこのプログラムは破棄することになる予感。
そんな風に思っいつつ冒頭にリンクした記事でPHP回答例を見てみると・・・。
なんか最終的に似た感じになっちゃった?
まあ折角書いたのでポチッと投稿。