一个很恶心的现象,不管你在一家公司工作多久,无论你多忠诚,多卖力,一旦公司赚的少了那你就成了眼中钉,肉中刺
刚刷到个贴子,说在公司干得再久、再卖力,一旦公司利润下滑,老员工就容易变成“眼中钉”。
我觉得这事吧,说到底还是职场的残酷逻辑:企业是逐利的机器,它看重的是“当下的价值”,而不是“曾经的忠诚”。有网友回帖说 loyalty 没用,我部分认同,但也不能全盘否定。毕竟稳定靠谱的人,本身就是价值的一种,只是很多老板算账时不一定看得见。
公司像开公交车,你付了车费,它会送你一程,但不会承诺送你到家。忠诚没错,但如果只寄希望于公司来回报,那就太冒险了。真正能靠得住的还是自己的能力和底牌。
所以与其抱怨不公,不如把眼光放长远:提升不可替代性,别让自己在“降本清单”上太显眼。【备注:文末可领最新资料】
算法题:可疑银行账户
昨天晚上十一点多,在公司楼下喝着便利店咖啡,运营又丢过来一堆转账流水,说“总觉得不对,你看看”。我一边打哈欠一边想,别上来就上算法大炮哈,先把问题拆小:谁在异常频率、异常路径、异常金额这三条线上扎眼,基本就八九不离十了。
思路就这几个点,别整花活
频率:短时间内被很多陌生账户“喂小单”(俗称拆分入金/Smurfing),紧接着一笔大额“吐出”,典型洗钱手法,对吧。 路径:钱绕一圈又回来了(3~4跳的小环),或者一号账户像“喷头”一样把钱泼向一堆新账户。 金额:夜间大额、贴边限额(比如刚好 49999)、或者日累计忽高忽低。
落地上我就做三件事:滑动窗口看频率、构图查小环、给账户打分(权重自己调)。别一下子追求完美,先把“最可疑的前100个”筛出来给风控。
import java.util.*;
import java.time.*;
classTx{
String from, to;
long cents; // 金额用分
long ts; // epoch millis
Tx(String f,String t,long c,long ts){this.from=f;this.to=t;this.cents=c;this.ts=ts;}
}
classSuspiciousDetector{
// 参数可调
long windowMs = Duration.ofHours(24).toMillis();
long smurfTotalMin = 3_000_00; // 24小时内小单总入金>=3000元
int smurfCountMin = 5; // 小单次数>=5
long smurfEachMax = 1_000_00; // 每笔小单<1000元
long bigOutMin = 5_000_00; // 随后单笔大额出金>=5000元
int newPayeeBurst = 10; // 24小时内新收款账户数阈值
// 主入口:返回账户->风险分
Map<String, Double> detect(List<Tx> list){
list.sort(Comparator.comparingLong(tx->tx.ts));
// 基础索引
Map<String, List<Tx>> inMap = new HashMap<>(); // 收入流水
Map<String, List<Tx>> outMap = new HashMap<>(); // 支出流水
Map<String, Set<String>> graph = new HashMap<>(); // 转账有向图
for (Tx t: list){
inMap.computeIfAbsent(t.to , k->new ArrayList<>()).add(t);
outMap.computeIfAbsent(t.from,k->new ArrayList<>()).add(t);
graph.computeIfAbsent(t.from,k->new HashSet<>()).add(t.to);
graph.putIfAbsent(t.to, graph.getOrDefault(t.to,new HashSet<>()));
}
Map<String, Double> score = new HashMap<>();
for (String acct: graph.keySet()){
double s = 0;
s += smurfingScore(acct, inMap.getOrDefault(acct, List.of()), outMap.getOrDefault(acct, List.of()));
s += cycleScore3(acct, graph); // 3环
s += newPayeeBurstScore(acct, outMap.getOrDefault(acct, List.of()));
s += nightLargeScore(outMap.getOrDefault(acct, List.of()));
if (s>0) score.put(acct, s);
}
return score;
}
// 规则1:Smurfing(小单密集入->大额出)
doublesmurfingScore(String acct, List<Tx> ins, List<Tx> outs){
Deque<Tx> q = new ArrayDeque<>();
long sum = 0; int cnt = 0; double best = 0;
int oi = 0;
for (Tx t: ins){
// 入金滑窗
q.addLast(t); sum+=t.cents; cnt++;
while (!q.isEmpty() && t.ts - q.peekFirst().ts > windowMs){
Tx x=q.pollFirst(); sum-=x.cents; cnt--;
}
// 窗内满足“小单密集”
if (cnt>=smurfCountMin && sum>=smurfTotalMin && allLessThan(q, smurfEachMax)){
// 找窗口结束后紧接着的大额出金(同一窗口末尾起算)
long endTs = q.peekLast().ts;
while (oi<outs.size() && outs.get(oi).ts<=endTs) oi++;
if (oi<outs.size() && outs.get(oi).cents>=bigOutMin && outs.get(oi).ts-endTs<=Duration.ofHours(6).toMillis()){
best = Math.max(best, 60.0); // 权重可调
}
}
}
return best;
}
booleanallLessThan(Deque<Tx> q, long cap){
for (Tx t: q) if (t.cents>=cap) returnfalse; returntrue;
}
// 规则2:长度=3的小环 a->b->c->a
doublecycleScore3(String start, Map<String, Set<String>> g){
if (!g.containsKey(start)) return0;
double s = 0;
for (String b: g.get(start)){
Set<String> gb = g.getOrDefault(b, Set.of());
for (String c: gb){
if (g.getOrDefault(c, Set.of()).contains(start) && !start.equals(b) && !b.equals(c) && !c.equals(start)){
s = Math.max(s, 40.0); // 发现即加分
}
}
}
return s;
}
// 规则3:24小时内首次出现的新收款账户过多(“喷头”式分发)
doublenewPayeeBurstScore(String acct, List<Tx> outs){
outs.sort(Comparator.comparingLong(x->x.ts));
Deque<Tx> q = new ArrayDeque<>();
Set<String> windowNew = new HashSet<>(), seen = new HashSet<>();
double best = 0;
for (Tx t: outs){
q.addLast(t);
// 进入窗口:如果没见过就是新收款人
if (!seen.contains(t.to)){ windowNew.add(t.to); }
while (!q.isEmpty() && t.ts - q.peekFirst().ts > windowMs){
Tx x = q.pollFirst();
// 窗口滑出只影响统计窗口,不改全局 seen
// 若需要更精细可加计数
}
best = Math.max(best, windowNew.size()>=newPayeeBurst ? 30.0 : 0.0);
seen.add(t.to);
}
return best;
}
// 规则4:夜间大额(例如 0:00~5:00)
doublenightLargeScore(List<Tx> outs){
double s=0;
for (Tx t: outs){
LocalDateTime dt = LocalDateTime.ofInstant(Instant.ofEpochMilli(t.ts), ZoneId.systemDefault());
int h = dt.getHour();
if (h<5 && t.cents>=bigOutMin) s = Math.max(s, 10.0);
}
return s;
}
}
先把一天的交易读进来,丢给 detect,拿到“账户->分数”。分数别当绝对值,是个排序线索。落地时再叠加黑名单、设备指纹、地理位置,效果更稳。有人问阈值怎么定?别拍脑袋,用历史真阳/真阴样本做网格搜索,或者直接把上面四个分项当特征喂给轻量模型(Logistic/GBDT),规矩与学习结合,鲁棒性就出来了。
我先回去续杯咖啡…你把阈值改成你们行规那套试试,命中率不行我们再加一条“限额贴边”的规则就够了。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html