风控排序评估(C++/Py/Java /Js/Go)题解
京东 2026年9月19号 笔试真题 第一题
题目内容
风控侧要对一批交易做欺诈打分。每笔交易有真实标签 tit_iti(000 表示正常,111 表示欺诈)以及模型给出的风险分 sis_isi(越大越像欺诈)。
请按 Mann-Whitney U 的秩统计来评估这批打分的排序质量,也就是 AUC。分数相同的交易必须用平均名次,不能随便拆开。
规则如下:
- 平均名次:把 mmm 笔交易按 sss 从小到大排。名次从 111 起。若连续若干笔分数相同,占住第 LLL 到第 RRR 个位置,则它们的名次都取 (L+R)/2(L+R)/2(L+R)/2。
- 由正类名次和还原 AUC:记欺诈笔数为 kkk,正常笔数为 ggg,欺诈样本的名次之和为 SSS。令 V=S−k(k+1)/2V = S - k(k+1)/2V=S−k(k+1)/2,AUC=V/(k⋅g)AUC = V/(k\cdot g)AUC=V/(k⋅g)。
输入描述
第一行一个整数 mmm,表示交易笔数。(2≤m≤19)(2 \le m \le 19)(2≤m≤19)
第二行 mmm 个整数 tit_iti,即标签,元素只能是 000 或 111。保证两种标签都出现。
第三行 mmm 个浮点数 sis_isi,即风险分。
输出描述
输出一行一个浮点数,表示 AUC,必须恰好保留 666 位小数。
样例1
输入
4
0 0 0 1
0.1 0.2 0.9 0.5
输出
0.666667
说明
- 按分数升序:(0.1,0)(0.1,0)(0.1,0) 名次 111,(0.2,0)(0.2,0)(0.2,0) 名次 222,(0.5,1)(0.5,1)(0.5,1) 名次 333,(0.9,0)(0.9,0)(0.9,0) 名次 444。
- 欺诈只有一笔,名次 S=3S=3S=3,k=1k=1k=1,g=3g=3g=3。
- V=3−1=2V=3-1=2V=3−1=2,AUC=2/3=0.666667AUC=2/3=0.666667AUC=2/3=0.666667。
样例2
输入
3
0 1 1
0.5 0.5 0.8
输出
0.750000
说明
- 两个 0.50.50.5 并列占第 111、222 名,平均名次都是 1.51.51.5;分数 0.80.80.8 的名次是 333。
- 欺诈名次和 S=1.5+3=4.5S=1.5+3=4.5S=1.5+3=4.5,k=2k=2k=2,g=1g=1g=1。
- V=4.5−3=1.5V=4.5-3=1.5V=4.5−3=1.5,AUC=1.5/2=0.750000AUC=1.5/2=0.750000AUC=1.5/2=0.750000。
题解和思路
思路
实现思路:模拟、排序
- 按照s从小到大进行排序,然后计算欺诈样本的平均名词之和。
- 将每笔交易保存为
(score,label) - 按照socre进行从小到大升序排序
- 统计欺诈样本和正常样本个数k, g
- 然后从前往后进行处理,每次处理相同分数排名片段,假设占据
L-R名,则对应名词为rank = (L + R) /2, 如果其中包含欺诈样本,则对应S + rank。 - 最后按照公式计算对应结果即可。
- 时间复杂度为
O(m)
C++
cpp
#include <bits/stdc++.h>
using namespace std;
struct Node {
double score;
int label;
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int m;
cin >> m;
vector<int> t(m);
for (int i = 0; i < m; i++) {
cin >> t[i];
}
vector<Node> a(m);
for (int i = 0; i < m; i++) {
cin >> a[i].score;
a[i].label = t[i];
}
sort(a.begin(), a.end(), [](const Node& x, const Node& y) {
return x.score < y.score;
});
int k = 0, g = 0;
for (auto& x : a) {
if (x.label == 1) k++;
else g++;
}
// S:欺诈样本的平均名次之和
double S = 0;
int i = 0;
while (i < m) {
int j = i;
// 找到分数相同的一组
while (j + 1 < m && a[j + 1].score == a[i].score) {
j++;
}
// 这一组占据第 i+1 到 j+1 名
double rank = ((i + 1) + (j + 1)) / 2.0;
// 相同分数的所有样本使用相同的平均名次
for (int p = i; p <= j; p++) {
if (a[p].label == 1) {
S += rank;
}
}
i = j + 1;
}
double V = S - 1.0 * k * (k + 1) / 2;
double auc = V / (1.0 * k * g);
cout << fixed << setprecision(6) << auc << '\n';
return 0;
}
Java
java
import java.util.*;
public class Main {
static class Node {
double score;
int label;
Node(double score, int label) {
this.score = score;
this.label = label;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int m = sc.nextInt();
int[] t = new int[m];
for (int i = 0; i < m; i++) {
t[i] = sc.nextInt();
}
Node[] a = new Node[m];
for (int i = 0; i < m; i++) {
double score = sc.nextDouble();
a[i] = new Node(score, t[i]);
}
Arrays.sort(a, (x, y) -> Double.compare(x.score, y.score));
int k = 0, g = 0;
for (Node x : a) {
if (x.label == 1) {
k++;
} else {
g++;
}
}
// S:欺诈样本的平均名次之和
double S = 0;
int i = 0;
while (i < m) {
int j = i;
// 找到分数相同的一组
while (j + 1 < m && a[j + 1].score == a[i].score) {
j++;
}
// 这一组占据第 i+1 到 j+1 名
double rank = ((i + 1) + (j + 1)) / 2.0;
// 相同分数的所有样本使用相同的平均名次
for (int p = i; p <= j; p++) {
if (a[p].label == 1) {
S += rank;
}
}
i = j + 1;
}
double V = S - 1.0 * k * (k + 1) / 2;
double auc = V / (1.0 * k * g);
System.out.printf("%.6f%n", auc);
}
}
python
python
import sys
class Node:
def __init__(self, score, label):
self.score = score
self.label = label
def main():
data = sys.stdin.read().split()
pos = 0
m = int(data[pos])
pos += 1
t = []
for i in range(m):
t.append(int(data[pos]))
pos += 1
a = []
for i in range(m):
score = float(data[pos])
pos += 1
a.append(Node(score, t[i]))
a.sort(key=lambda x: x.score)
k = 0
g = 0
for x in a:
if x.label == 1:
k += 1
else:
g += 1
# S:欺诈样本的平均名次之和
S = 0.0
i = 0
while i < m:
j = i
# 找到分数相同的一组
while j + 1 < m and a[j + 1].score == a[i].score:
j += 1
# 这一组占据第 i+1 到 j+1 名
rank = ((i + 1) + (j + 1)) / 2.0
# 相同分数的所有样本使用相同的平均名次
for p in range(i, j + 1):
if a[p].label == 1:
S += rank
i = j + 1
V = S - 1.0 * k * (k + 1) / 2
auc = V / (1.0 * k * g)
print(f"{auc:.6f}")
if __name__ == "__main__":
main()
Javascript
js
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const input = [];
rl.on('line', line => {
input.push(...line.trim().split(/\s+/));
});
rl.on('close', () => {
let pos = 0;
const m = Number(input[pos++]);
const t = [];
for (let i = 0; i < m; i++) {
t.push(Number(input[pos++]));
}
const a = [];
for (let i = 0; i < m; i++) {
const score = Number(input[pos++]);
a.push({
score: score,
label: t[i]
});
}
a.sort((x, y) => x.score - y.score);
let k = 0;
let g = 0;
for (const x of a) {
if (x.label === 1) {
k++;
} else {
g++;
}
}
// S:欺诈样本的平均名次之和
let S = 0;
let i = 0;
while (i < m) {
let j = i;
// 找到分数相同的一组
while (j + 1 < m && a[j + 1].score === a[i].score) {
j++;
}
// 这一组占据第 i+1 到 j+1 名
const rank = ((i + 1) + (j + 1)) / 2.0;
// 相同分数的所有样本使用相同的平均名次
for (let p = i; p <= j; p++) {
if (a[p].label === 1) {
S += rank;
}
}
i = j + 1;
}
const V = S - 1.0 * k * (k + 1) / 2;
const auc = V / (1.0 * k * g);
console.log(auc.toFixed(6));
});
Go
go
package main
import (
"bufio"
"fmt"
"os"
"sort"
)
type Node struct {
score float64
label int
}
func main() {
in := bufio.NewReader(os.Stdin)
out := bufio.NewWriter(os.Stdout)
defer out.Flush()
var m int
fmt.Fscan(in, &m)
t := make([]int, m)
for i := 0; i < m; i++ {
fmt.Fscan(in, &t[i])
}
a := make([]Node, m)
for i := 0; i < m; i++ {
fmt.Fscan(in, &a[i].score)
a[i].label = t[i]
}
sort.Slice(a, func(i, j int) bool {
return a[i].score < a[j].score
})
k, g := 0, 0
for _, x := range a {
if x.label == 1 {
k++
} else {
g++
}
}
// S:欺诈样本的平均名次之和
S := 0.0
i := 0
for i < m {
j := i
// 找到分数相同的一组
for j+1 < m && a[j+1].score == a[i].score {
j++
}
// 这一组占据第 i+1 到 j+1 名
rank := float64((i+1)+(j+1)) / 2.0
// 相同分数的所有样本使用相同的平均名次
for p := i; p <= j; p++ {
if a[p].label == 1 {
S += rank
}
}
i = j + 1
}
V := S - 1.0*float64(k*(k+1))/2
auc := V / (1.0 * float64(k*g))
fmt.Fprintf(out, "%.6f\n", auc)
}