前置:经济选择与经济分析 · 分工、比较优势与交换
早餐店把价格提高一元,收入会增加吗?先别急着回答。店主可能少卖,也可能恰好遇到新写字楼开业、多了一批顾客。单看价格与销量的结果,未必能分清这些变化。本篇先解释谁愿意交易、价格怎样协调计划,再问数量的反应究竟有多大。
以下都是虚构的教学市场。我们暂时固定商品质量、收入和交易规则,不计税及第三方影响;之后会逐个放回这些条件。区分“只改价格”与“其他条件也变了”,是读懂供需图和弹性的共同前提。
四位买家和四位卖家会怎样选
每位买家最多买一份早餐,最高愿付金额依次是 12、10、8、6;四位独立卖家各能做一份,机会成本是 2、4、6、8。金额均为元,数量为份。先试价格 5:四位买家都想买,只有成本 2、4 的两位卖家愿意卖。有买家拿着钱也买不到。
这里买方估值须严格高于价格,卖方成本须严格低于价格;刚好相等的人无差异,下表避开这个边界。
| 试出的价格 | 想买且买得起的人 | 愿意提供的人 | 如果只按这个价格交易 |
| 5 | 4 | 2 | 最多两份,买方相互竞争 |
| 7 | 3 | 3 | 三份交易可以使双方计划相容 |
| 9 | 2 | 4 | 最多两份,部分卖家没有订单 |
设商品相同、信息可得、价格能变动。在价格 5 时,没买到的高估值买家可能愿意加价,原本不做早餐的卖家也可能加入;在价格 9 时,卖不掉的人可能降价争取订单。到 7,三位估值高于 7 的买家与三位成本低于 7 的卖家能成交,再多一份会让成本 8 超过剩余买家的估值 6。
这解释了价格调整的压力,并没有证明现实每天一定沿这条路径移动。若早餐已经做好、店铺不能随时改价,短期结果可能是排队或剩货。所谓均衡是模型中买卖计划相容,不是“人人满意”或“分配公平”。钱少的人即使很需要早餐,也可能没有高货币支付意愿。
从个人选择到一条需求关系
现在把价格从低到高依次试过去,记下每一价位的购买人数。价格跨过某人的最高愿付值时,他退出;因此少数参与者会形成阶梯,而不是天然光滑的线。供给同理:价格超过一位卖家的机会成本时,他加入。
参与者足够多、差异足够细时,可以用连续关系近似这些阶梯。下面另设一个较大的早餐市场,在相关价格区间采用简化线性关系:
需求量 Qd = 120 − 10P
供给量 Qs = 20 + 10P
这不是从上面八个人精确推导出的方程,也不是统计估计。它是接下来共同使用的教学模型;P 每增加 1 元,其他需求条件不变时少买 10 份,而卖方愿多供 10 份。不要把局部近似任意外推到负价格或不可能的数量。
令两边相等,120−10P = 20+10P,得到 P=5、Q=70。核对一次:需求是 120−50=70,供给是 20+50=70。若价格暂时只有 4,需求 80、供给 60;差额 20 是未满足的购买计划,不是已经卖出的数量。价格为 6 时则有 20 份超额供给。
为什么价量能同时上升
假设写字楼开业,在每个价格下都多出 20 份需求。现在是 Qd=140−10P,原供给关系不变。解得新均衡 P=6、Q=80。价格更高、成交更多,并没有推翻“在其他条件固定时,涨价减少需求量”。我们比较的是两条不同需求关系上的点。
试着用一句话分辨:店主只把标价从 5 改为 6,顾客其他条件不变,是沿原需求关系从 70 走到 60;新顾客加入则使每个价格下的需求都变化,是整条关系移动。不要因为现实里价格变了,就自动断定只能发生前一种变化。
正在呈现知识画面
改变条件,核对结果基准 P=5、Q=70;需求增加后为 6、80;供给减少后为 6、60。每次只改变一种条件。
{"id":"economics-05","title":"改变条件,核对结果","summary":"基准 P=5、Q=70;需求增加后为 6、80;供给减少后为 6、60。每次只改变一种条件。","height":750,"html":"<div class=\"controls\"><label for=\"scenario\"><span data-i18n=\"scenario\"></span><select id=\"scenario\"><option value=\"0\" selected data-i18n=\"baseline\"></option><option value=\"1\" data-i18n=\"moreDemand\"></option><option value=\"2\" data-i18n=\"lessSupply\"></option></select></label></div><button id=\"reset\" type=\"button\" data-i18n=\"reset\"></button><div id=\"graphic\"></div><div id=\"results\" class=\"results\" role=\"status\" aria-live=\"polite\" data-i18n-label=\"result\"></div><p class=\"scope\" data-i18n=\"scope\"></p>","css":"body{padding:20px}h2{font-size:19px;margin:0 0 14px}.controls{display:grid;gap:16px}label{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px;font-size:15px}select,input{grid-column:1/-1;width:100%;min-width:0;font:inherit}select{padding:10px;background:var(--paper);color:var(--ink);border:1px solid var(--rule);border-radius:6px;white-space:normal}input{accent-color:var(--accent)}button{min-height:44px;margin:16px 0;font:inherit}.plot{display:block;width:100%;max-width:560px;height:auto;margin:auto}.plot text{fill:var(--ink);font-size:14px}.legend{display:flex;gap:12px;flex-wrap:wrap;margin:8px 0;font-size:14px}.legend i{display:inline-block;width:16px;height:3px;margin-right:5px;vertical-align:middle}.results{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:16px}.metric{border:1px solid var(--rule);padding:10px;border-radius:6px;min-width:0}.metric span{display:block;font-size:14px;min-height:3.5em;overflow-wrap:anywhere}.metric strong{font-size:22px;font-weight:600}.barrow{margin:14px 0;font-size:15px}.barlabel{display:flex;justify-content:space-between;gap:10px}.bar{height:16px;background:var(--surface);margin-top:5px}.bar i{display:block;height:100%;background:var(--accent)}.stack{display:flex;height:35px;margin:15px 0}.stack span{display:flex;align-items:center;justify-content:center;color:var(--paper);font-size:13px}.scope{font-size:14px;color:var(--muted);margin:14px 0 0;line-height:1.65}@media(max-width:380px){body{padding:14px 10px}.results{gap:6px}.metric{padding:8px}.metric strong{font-size:19px}}.accounts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.account{border:1px solid var(--rule);border-radius:8px;padding:12px;min-width:0}.account h3{font-size:16px;margin:0 0 12px}.account dl{margin:0}.account dt{font-size:13px;overflow-wrap:anywhere}.account dd{margin:2px 0 12px;font-variant-numeric:tabular-nums}.account .changed{font-weight:700;color:var(--accent)}.area-legend{font-size:14px;line-height:1.7}.area-legend span{display:block}.area-legend i{display:inline-block;width:12px;height:12px;margin-right:6px}@media(max-width:480px){.accounts{grid-template-columns:1fr}}","js":"const N=5,DEFAULTS={\"scenario\": 0};\n/* Pure, deterministic teaching models. Values are synthetic, not estimates. */\nfunction economy(n,p){\n if(n===6){const q=p.remaining,P0=10,P1=12,Q0=100;return{metrics:{oldRevenue:P0*Q0,newRevenue:P1*q,retainedGain:(P1-P0)*q,lostRevenue:P0*(Q0-q),elasticity:Math.abs(((q-Q0)/((q+Q0)/2))/((P1-P0)/((P1+P0)/2)))},revenueAreas:{remaining:q,oldPrice:P0,newPrice:P1,oldQuantity:Q0}};}\n if(n===5){const shift=p.scenario===1?20:0,supply=p.scenario===2?-20:0,P=(100+shift-supply)/20,Q=20+supply+10*P;return{metrics:{price:P,quantity:Q},plot:{xmax:140,ymax:14,x:'quantity',y:'price',lines:[{key:'demand',points:[[0,(120+shift)/10],[120+shift,0]]},{key:'supply',points:[[Math.max(0,20+supply),0],[140,(120-supply)/10]]},{key:'base',points:[[0,5],[140,5]],dash:true}],dot:[Q,P]}};}\n if(n===8){const b=p.response,bs=10,t=p.tax,Pb=5+bs*t/(b+bs),Ps=5-b*t/(b+bs),Q=70-b*bs*t/(b+bs);return{metrics:{buyer:Pb,seller:Ps,quantity:Q,revenue:t*Q,loss:.5*t*(70-Q)},plot:{xmax:100,ymax:20,x:'quantity',y:'price',areas:[{key:'revenue',points:[[0,Ps],[Q,Ps],[Q,Pb],[0,Pb]]},{key:'loss',points:[[Q,Ps],[Q,Pb],[70,5]]}],lines:[{key:'demand',points:[[0,5+70/b],[Math.min(100,70+5*b),Math.max(0,5+(70-100)/b)]]},{key:'supply',points:[[20,0],[100,8]]},{key:'base',points:[[0,5],[100,5]],dash:true},{key:'wedge',points:[[Q,Ps],[Q,Pb]]}],dot:[Q,Pb]}};}\n if(n===18){let index=100+p.weight*p.rise/100;return{metrics:{index,inflation:index-100},bars:[{key:'base',v:100},{key:'new',v:index}],max:180};}\n if(n===19){let E=60,U=10,N=30;if(p.scenario===1){E+=5;U-=5;}if(p.scenario===2){U-=5;N+=5;}if(p.scenario===3){U+=5;N-=5;}return{metrics:{employment:E,unemployment:U,participation:100*(E+U)/100,urate:100*U/(E+U),erate:E},stack:[E,U,N],stackKeys:['employment','unemployment','outside']};}\n if(n===20){const step=p.scenario;let reserves=100,loan=0,deposit=80,equity=20,br=0,bd=0;if(step!==0){loan=60;deposit=140;}if(step===2){reserves-=60;deposit-=60;br=60;bd=60;}if(step===3){loan=40;deposit=120;}const vals={reserves,loan,deposit,equity,bankBreserves:br,bankBdeposits:bd,borrowerDeposit:step===2?0:loan,borrowerGoods:step===2?60:0,borrowerDebt:loan,merchantDeposit:bd,merchantGoods:step===2?0:60};const prev=step>1?economy(20,{scenario:1}).accounts:economy20initial();return{metrics:{reserves,loan,deposit,equity,bankBreserves:br,bankBdeposits:bd,assets:reserves+loan,liabilities:deposit+equity},accounts:vals,ledger:[['bankA',['reserves','loan','deposit','equity']],['bankB',['bankBreserves','bankBdeposits']],['borrower',['borrowerDeposit','borrowerGoods','borrowerDebt']],['merchant',['merchantDeposit','merchantGoods']]].map(([title,keys])=>({title,items:keys.map(key=>({key,value:vals[key],delta:vals[key]-prev[key]}))}))};}\n if(n===22){const c=p.consume,shock=p.shock,rounds=Array.from({length:8},(_,k)=>shock*c**k);return{metrics:{total:shock/(1-c),shown:rounds.reduce((a,b)=>a+b,0),multiplier:1/(1-c)},bars:rounds.map((v,k)=>({label:String(k+1),v})),max:100,rounds};}\n if(n===25){const e=p.rate;return{metrics:{importBill:10*e,exportBill:70/e,debtBill:100*e},bills:true};}\n if(n===26){const A=p.tech,s=p.save/100,d=.1,k=(s*A/d)**2,y=A*Math.sqrt(k),trajectory=[];let current=25;for(let t=0;t<=60;t++){const output=A*Math.sqrt(current),investment=s*output,depreciation=d*current;trajectory.push({t,k:current,y:output,consumption:(1-s)*output,investment,depreciation});current+=investment-depreciation;}return{metrics:{capital:k,output:y,consumption:(1-s)*y},trajectory,plot:{xmax:60,ymax:Math.max(10,...trajectory.map(a=>a.y))*1.1,x:'period',y:'perPersonFlow',lines:[{key:'baselineConsumption',points:[[0,7.5],[60,7.5]],dash:true},{key:'outputPath',points:trajectory.map(a=>[a.t,a.y])},{key:'consumptionPath',points:trajectory.map(a=>[a.t,a.consumption])}]}};}\n throw Error('Unknown lesson');\n}\nfunction economy20initial(){return{reserves:100,loan:0,deposit:80,equity:20,bankBreserves:0,bankBdeposits:0,borrowerDeposit:0,borrowerGoods:0,borrowerDebt:0,merchantDeposit:0,merchantGoods:60};}\nif(typeof module!=='undefined')module.exports={economy};\n\nfunction renderTeachingView(r,el){\n if(r.ledger){\n el.innerHTML='<div class=\"accounts\">'+r.ledger.map(a=>'<section class=\"account\"><h3>'+escape(viz.t(a.title))+'</h3><dl>'+a.items.map(v=>`<dt>${escape(viz.t(v.key))}</dt><dd class=\"${v.delta?'changed':''}\">${viz.number(v.value)} (${v.delta>0?'+':''}${viz.number(v.delta)})</dd>`).join('')+'</dl></section>').join('')+'</div><p class=\"scope\">'+escape(viz.t('delta'))+'</p>';\n $('#results').hidden=true;\n }\n if(r.revenueAreas){const a=r.revenueAreas,x=q=>45+q*2.65,y=p=>213-p*14,q=a.remaining;\n let s=`<svg class=\"plot\" viewBox=\"0 0 360 270\" role=\"img\" aria-label=\"${escape(viz.t('figure'))}\"><path d=\"M45 28V213H322\" fill=\"none\" stroke=\"var(--rule)\"/>`;\n const rect=(x0,x1,p0,p1,c)=>`<rect x=\"${x(x0)}\" y=\"${y(p1)}\" width=\"${(x1-x0)*2.65}\" height=\"${(p1-p0)*14}\" fill=\"${c}\" fill-opacity=\".48\" stroke=\"${c}\"/>`;\n s+=rect(0,q,0,10,'#5277ab')+rect(0,q,10,12,'#247756')+rect(q,100,0,10,'#b56838');\n for(const p of[0,10,12])s+=`<text x=\"37\" y=\"${y(p)+4}\" text-anchor=\"end\">${p}</text>`;\n for(const v of[0,50,100])s+=`<text x=\"${x(v)}\" y=\"235\" text-anchor=\"middle\">${v}</text>`;\n s+=`<text x=\"45\" y=\"20\">${escape(viz.t('price'))}</text><text x=\"310\" y=\"261\" text-anchor=\"end\">${escape(viz.t('quantity'))}</text></svg>`;\n el.innerHTML=s+'<div class=\"area-legend\">'+[['retainedBase','#5277ab'],['retainedGain','#247756'],['lostRevenue','#b56838']].map(([k,c])=>`<span><i style=\"background:${c}\"></i>${escape(viz.t(k))}</span>`).join('')+'</div>';\n }\n}\n\nconst $=s=>document.querySelector(s),col=['var(--accent)','var(--second)','#7581ae','#b36b47'];\nconst escape=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nfunction render(){\n let p={};for(const k of Object.keys(DEFAULTS)){p[k]=Number($('#'+k).value);let o=$('#out-'+k);if(o)o.textContent=viz.number(p[k]);}\n const r=economy(N,p);document.body.dataset.result=JSON.stringify(r.metrics);document.body.dataset.params=JSON.stringify(p);\n $('#results').innerHTML=Object.entries(r.metrics).map(([k,v])=>`<div class=\"metric\"><span>${escape(viz.t(k))}</span><strong data-metric=\"${k}\">${viz.number(v,2)}</strong></div>`).join('');\n const el=$('#graphic');el.replaceChildren();\n if(r.plot){const a=r.plot,x=q=>48+q/a.xmax*278,y=v=>205-v/a.ymax*164;\n let svg=`<svg class=\"plot\" viewBox=\"0 0 360 258\" role=\"img\" aria-label=\"${escape(viz.t('figure'))}\"><path d=\"M48 30V205H333\" fill=\"none\" stroke=\"var(--rule)\"/>`;\n for(const q of [0,a.xmax/2,a.xmax])svg+=`<text x=\"${x(q)}\" y=\"226\" text-anchor=\"middle\">${viz.number(q,1)}</text>`;\n for(const v of [0,a.ymax/2,a.ymax])svg+=`<text x=\"39\" y=\"${y(v)+4}\" text-anchor=\"end\">${viz.number(v,1)}</text>`;\n svg+=`<text x=\"48\" y=\"20\">${escape(viz.t(a.y))}</text><text x=\"326\" y=\"251\" text-anchor=\"end\">${escape(viz.t(a.x))}</text>`;\n (a.areas||[]).forEach((region,j)=>{svg+=`<polygon points=\"${region.points.map(([xx,yy])=>x(xx)+','+y(yy)).join(' ')}\" fill=\"${j===0?'#4c8c9d':'#bd7446'}\" fill-opacity=\".30\"/>`;});\n a.lines.forEach((l,j)=>{svg+=`<polyline fill=\"none\" stroke=\"${col[j]}\" stroke-width=\"${l.key==='wedge'?5:2.5}\" ${l.dash?'stroke-dasharray=\"5 4\"':''} points=\"${l.points.map(([xx,yy])=>x(xx)+','+y(yy)).join(' ')}\"/>`;});\n if(a.dot)svg+=`<circle cx=\"${x(a.dot[0])}\" cy=\"${y(a.dot[1])}\" r=\"5\" fill=\"var(--ink)\"/>`;\n el.innerHTML=svg+'</svg><div class=\"legend\">'+a.lines.map((l,j)=>`<span><i style=\"background:${col[j]}\"></i>${escape(viz.t(l.key))}</span>`).join('')+(a.areas||[]).map((a,j)=>`<span><i style=\"height:10px;background:${j===0?'#4c8c9d':'#bd7446'}\"></i>${escape(viz.t(a.key))}</span>`).join('')+'</div>';\n }else if(r.bars){el.innerHTML=r.bars.map(b=>`<div class=\"barrow\"><div class=\"barlabel\"><span>${escape(b.key?viz.t(b.key):viz.t('round')+' '+b.label)}</span><strong>${viz.number(b.v)}</strong></div><div class=\"bar\"><i style=\"width:${Math.abs(b.v)/Math.max(r.max,...r.bars.map(a=>Math.abs(a.v)))*100}%\"></i></div></div>`).join('')+(N===22?`<p class=\"scope\">${escape(viz.t('negative'))}</p>`:'');\n }else if(r.stack){el.innerHTML='<div class=\"stack\">'+r.stack.map((v,j)=>`<span style=\"width:${v}%;background:${col[j]}\">${v}</span>`).join('')+'</div><div class=\"legend\">'+r.stackKeys.map((k,j)=>`<span><i style=\"background:${col[j]}\"></i>${escape(viz.t(k))}</span>`).join('')+'</div>';}\nrenderTeachingView(r,el);\n}\nfor(const k of Object.keys(DEFAULTS))$('#'+k).addEventListener('input',render);\n$('#reset').addEventListener('click',()=>{for(const[k,v]of Object.entries(DEFAULTS))$('#'+k).value=v;render();});render();\n","audio":false,"strings":{"base":"基准","baseline":"保持基准","demand":"需求","figure":"当前参数下的模型图;准确值见下方结果。","lessSupply":"供给减少 20","moreDemand":"需求增加 20","price":"单位价格 P","quantity":"数量 Q","reset":"恢复基准","result":"计算结果","scenario":"选择一种变化","scope":"基准 P=5、Q=70;需求增加后为 6、80;供给减少后为 6、60。每次只改变一种条件。","supply":"供给"}}
先预测“新增顾客”和“供应减少”是否都会推高价格,再切换。供应减少 20 时,新均衡为 6、60;两种变化的价格方向相同,数量方向相反。若同时发生,数量取决于幅度,不能只凭一句“成本涨了、需求也旺”判断。
价格上限也能用同一模型追踪:上限有效约束在 4 时,愿买 80、愿卖 60。即使 60 份全部匹配,仍有人买不到。政策评价还要追问谁能得到、排队是否耗时、补充供给怎样实现,而不是把 80 份需求当作实际消费或直接把短缺当作全部评价。
少卖一些,为什么收入还能增加
价格方向已经清楚,但“少卖”有多大,决定提价后的营业收入。现在聚焦一家店的一次定价比较,明确切换到另一组店级需求设定:原价 10、卖 100 份,营业收入 1000;新价 12。店铺需求与整个市场需求不同,顾客能否转去别家是关键条件。
先猜:如果还卖 90 份,是否比原来收入少?把变化拆成两部分,就不必凭直觉猜总数。
- 留下的 90 份,每份多收 2 元,多收 180。
- 消失的 10 份,失去原来每份 10 元的支付,少收 100。
- 两者相抵,收入净增 80;直接核对
12×90=1080。
若只有 70 份留下,多收 140,却少收 300,净减 160,收入变为 840。注意少卖的部分按原价格计价;若两部分都用新价,会重复计算变化。
正在呈现知识画面
改变条件,核对结果价格从 10 到 12,原销量 100。留下 90 件时多收 180、少收 100,收入净增 80;留下 70 件时净减 160。蓝色是保留部分,绿色是多收,橙色是失去。
{"id":"economics-06","title":"改变条件,核对结果","summary":"价格从 10 到 12,原销量 100。留下 90 件时多收 180、少收 100,收入净增 80;留下 70 件时净减 160。蓝色是保留部分,绿色是多收,橙色是失去。","height":750,"html":"<div class=\"controls\"><label for=\"remaining\"><span data-i18n=\"remaining\"></span><output id=\"out-remaining\" for=\"remaining\"></output><input id=\"remaining\" type=\"range\" min=\"60\" max=\"100\" step=\"1\" value=\"90\"></label></div><button id=\"reset\" type=\"button\" data-i18n=\"reset\"></button><div id=\"graphic\"></div><div id=\"results\" class=\"results\" role=\"status\" aria-live=\"polite\" data-i18n-label=\"result\"></div><p class=\"scope\" data-i18n=\"scope\"></p>","css":"body{padding:20px}h2{font-size:19px;margin:0 0 14px}.controls{display:grid;gap:16px}label{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px;font-size:15px}select,input{grid-column:1/-1;width:100%;min-width:0;font:inherit}select{padding:10px;background:var(--paper);color:var(--ink);border:1px solid var(--rule);border-radius:6px;white-space:normal}input{accent-color:var(--accent)}button{min-height:44px;margin:16px 0;font:inherit}.plot{display:block;width:100%;max-width:560px;height:auto;margin:auto}.plot text{fill:var(--ink);font-size:14px}.legend{display:flex;gap:12px;flex-wrap:wrap;margin:8px 0;font-size:14px}.legend i{display:inline-block;width:16px;height:3px;margin-right:5px;vertical-align:middle}.results{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:16px}.metric{border:1px solid var(--rule);padding:10px;border-radius:6px;min-width:0}.metric span{display:block;font-size:14px;min-height:3.5em;overflow-wrap:anywhere}.metric strong{font-size:22px;font-weight:600}.barrow{margin:14px 0;font-size:15px}.barlabel{display:flex;justify-content:space-between;gap:10px}.bar{height:16px;background:var(--surface);margin-top:5px}.bar i{display:block;height:100%;background:var(--accent)}.stack{display:flex;height:35px;margin:15px 0}.stack span{display:flex;align-items:center;justify-content:center;color:var(--paper);font-size:13px}.scope{font-size:14px;color:var(--muted);margin:14px 0 0;line-height:1.65}@media(max-width:380px){body{padding:14px 10px}.results{gap:6px}.metric{padding:8px}.metric strong{font-size:19px}}.accounts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.account{border:1px solid var(--rule);border-radius:8px;padding:12px;min-width:0}.account h3{font-size:16px;margin:0 0 12px}.account dl{margin:0}.account dt{font-size:13px;overflow-wrap:anywhere}.account dd{margin:2px 0 12px;font-variant-numeric:tabular-nums}.account .changed{font-weight:700;color:var(--accent)}.area-legend{font-size:14px;line-height:1.7}.area-legend span{display:block}.area-legend i{display:inline-block;width:12px;height:12px;margin-right:6px}@media(max-width:480px){.accounts{grid-template-columns:1fr}}","js":"const N=6,DEFAULTS={\"remaining\": 90};\n/* Pure, deterministic teaching models. Values are synthetic, not estimates. */\nfunction economy(n,p){\n if(n===6){const q=p.remaining,P0=10,P1=12,Q0=100;return{metrics:{oldRevenue:P0*Q0,newRevenue:P1*q,retainedGain:(P1-P0)*q,lostRevenue:P0*(Q0-q),elasticity:Math.abs(((q-Q0)/((q+Q0)/2))/((P1-P0)/((P1+P0)/2)))},revenueAreas:{remaining:q,oldPrice:P0,newPrice:P1,oldQuantity:Q0}};}\n if(n===5){const shift=p.scenario===1?20:0,supply=p.scenario===2?-20:0,P=(100+shift-supply)/20,Q=20+supply+10*P;return{metrics:{price:P,quantity:Q},plot:{xmax:140,ymax:14,x:'quantity',y:'price',lines:[{key:'demand',points:[[0,(120+shift)/10],[120+shift,0]]},{key:'supply',points:[[Math.max(0,20+supply),0],[140,(120-supply)/10]]},{key:'base',points:[[0,5],[140,5]],dash:true}],dot:[Q,P]}};}\n if(n===8){const b=p.response,bs=10,t=p.tax,Pb=5+bs*t/(b+bs),Ps=5-b*t/(b+bs),Q=70-b*bs*t/(b+bs);return{metrics:{buyer:Pb,seller:Ps,quantity:Q,revenue:t*Q,loss:.5*t*(70-Q)},plot:{xmax:100,ymax:20,x:'quantity',y:'price',areas:[{key:'revenue',points:[[0,Ps],[Q,Ps],[Q,Pb],[0,Pb]]},{key:'loss',points:[[Q,Ps],[Q,Pb],[70,5]]}],lines:[{key:'demand',points:[[0,5+70/b],[Math.min(100,70+5*b),Math.max(0,5+(70-100)/b)]]},{key:'supply',points:[[20,0],[100,8]]},{key:'base',points:[[0,5],[100,5]],dash:true},{key:'wedge',points:[[Q,Ps],[Q,Pb]]}],dot:[Q,Pb]}};}\n if(n===18){let index=100+p.weight*p.rise/100;return{metrics:{index,inflation:index-100},bars:[{key:'base',v:100},{key:'new',v:index}],max:180};}\n if(n===19){let E=60,U=10,N=30;if(p.scenario===1){E+=5;U-=5;}if(p.scenario===2){U-=5;N+=5;}if(p.scenario===3){U+=5;N-=5;}return{metrics:{employment:E,unemployment:U,participation:100*(E+U)/100,urate:100*U/(E+U),erate:E},stack:[E,U,N],stackKeys:['employment','unemployment','outside']};}\n if(n===20){const step=p.scenario;let reserves=100,loan=0,deposit=80,equity=20,br=0,bd=0;if(step!==0){loan=60;deposit=140;}if(step===2){reserves-=60;deposit-=60;br=60;bd=60;}if(step===3){loan=40;deposit=120;}const vals={reserves,loan,deposit,equity,bankBreserves:br,bankBdeposits:bd,borrowerDeposit:step===2?0:loan,borrowerGoods:step===2?60:0,borrowerDebt:loan,merchantDeposit:bd,merchantGoods:step===2?0:60};const prev=step>1?economy(20,{scenario:1}).accounts:economy20initial();return{metrics:{reserves,loan,deposit,equity,bankBreserves:br,bankBdeposits:bd,assets:reserves+loan,liabilities:deposit+equity},accounts:vals,ledger:[['bankA',['reserves','loan','deposit','equity']],['bankB',['bankBreserves','bankBdeposits']],['borrower',['borrowerDeposit','borrowerGoods','borrowerDebt']],['merchant',['merchantDeposit','merchantGoods']]].map(([title,keys])=>({title,items:keys.map(key=>({key,value:vals[key],delta:vals[key]-prev[key]}))}))};}\n if(n===22){const c=p.consume,shock=p.shock,rounds=Array.from({length:8},(_,k)=>shock*c**k);return{metrics:{total:shock/(1-c),shown:rounds.reduce((a,b)=>a+b,0),multiplier:1/(1-c)},bars:rounds.map((v,k)=>({label:String(k+1),v})),max:100,rounds};}\n if(n===25){const e=p.rate;return{metrics:{importBill:10*e,exportBill:70/e,debtBill:100*e},bills:true};}\n if(n===26){const A=p.tech,s=p.save/100,d=.1,k=(s*A/d)**2,y=A*Math.sqrt(k),trajectory=[];let current=25;for(let t=0;t<=60;t++){const output=A*Math.sqrt(current),investment=s*output,depreciation=d*current;trajectory.push({t,k:current,y:output,consumption:(1-s)*output,investment,depreciation});current+=investment-depreciation;}return{metrics:{capital:k,output:y,consumption:(1-s)*y},trajectory,plot:{xmax:60,ymax:Math.max(10,...trajectory.map(a=>a.y))*1.1,x:'period',y:'perPersonFlow',lines:[{key:'baselineConsumption',points:[[0,7.5],[60,7.5]],dash:true},{key:'outputPath',points:trajectory.map(a=>[a.t,a.y])},{key:'consumptionPath',points:trajectory.map(a=>[a.t,a.consumption])}]}};}\n throw Error('Unknown lesson');\n}\nfunction economy20initial(){return{reserves:100,loan:0,deposit:80,equity:20,bankBreserves:0,bankBdeposits:0,borrowerDeposit:0,borrowerGoods:0,borrowerDebt:0,merchantDeposit:0,merchantGoods:60};}\nif(typeof module!=='undefined')module.exports={economy};\n\nfunction renderTeachingView(r,el){\n if(r.ledger){\n el.innerHTML='<div class=\"accounts\">'+r.ledger.map(a=>'<section class=\"account\"><h3>'+escape(viz.t(a.title))+'</h3><dl>'+a.items.map(v=>`<dt>${escape(viz.t(v.key))}</dt><dd class=\"${v.delta?'changed':''}\">${viz.number(v.value)} (${v.delta>0?'+':''}${viz.number(v.delta)})</dd>`).join('')+'</dl></section>').join('')+'</div><p class=\"scope\">'+escape(viz.t('delta'))+'</p>';\n $('#results').hidden=true;\n }\n if(r.revenueAreas){const a=r.revenueAreas,x=q=>45+q*2.65,y=p=>213-p*14,q=a.remaining;\n let s=`<svg class=\"plot\" viewBox=\"0 0 360 270\" role=\"img\" aria-label=\"${escape(viz.t('figure'))}\"><path d=\"M45 28V213H322\" fill=\"none\" stroke=\"var(--rule)\"/>`;\n const rect=(x0,x1,p0,p1,c)=>`<rect x=\"${x(x0)}\" y=\"${y(p1)}\" width=\"${(x1-x0)*2.65}\" height=\"${(p1-p0)*14}\" fill=\"${c}\" fill-opacity=\".48\" stroke=\"${c}\"/>`;\n s+=rect(0,q,0,10,'#5277ab')+rect(0,q,10,12,'#247756')+rect(q,100,0,10,'#b56838');\n for(const p of[0,10,12])s+=`<text x=\"37\" y=\"${y(p)+4}\" text-anchor=\"end\">${p}</text>`;\n for(const v of[0,50,100])s+=`<text x=\"${x(v)}\" y=\"235\" text-anchor=\"middle\">${v}</text>`;\n s+=`<text x=\"45\" y=\"20\">${escape(viz.t('price'))}</text><text x=\"310\" y=\"261\" text-anchor=\"end\">${escape(viz.t('quantity'))}</text></svg>`;\n el.innerHTML=s+'<div class=\"area-legend\">'+[['retainedBase','#5277ab'],['retainedGain','#247756'],['lostRevenue','#b56838']].map(([k,c])=>`<span><i style=\"background:${c}\"></i>${escape(viz.t(k))}</span>`).join('')+'</div>';\n }\n}\n\nconst $=s=>document.querySelector(s),col=['var(--accent)','var(--second)','#7581ae','#b36b47'];\nconst escape=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nfunction render(){\n let p={};for(const k of Object.keys(DEFAULTS)){p[k]=Number($('#'+k).value);let o=$('#out-'+k);if(o)o.textContent=viz.number(p[k]);}\n const r=economy(N,p);document.body.dataset.result=JSON.stringify(r.metrics);document.body.dataset.params=JSON.stringify(p);\n $('#results').innerHTML=Object.entries(r.metrics).map(([k,v])=>`<div class=\"metric\"><span>${escape(viz.t(k))}</span><strong data-metric=\"${k}\">${viz.number(v,2)}</strong></div>`).join('');\n const el=$('#graphic');el.replaceChildren();\n if(r.plot){const a=r.plot,x=q=>48+q/a.xmax*278,y=v=>205-v/a.ymax*164;\n let svg=`<svg class=\"plot\" viewBox=\"0 0 360 258\" role=\"img\" aria-label=\"${escape(viz.t('figure'))}\"><path d=\"M48 30V205H333\" fill=\"none\" stroke=\"var(--rule)\"/>`;\n for(const q of [0,a.xmax/2,a.xmax])svg+=`<text x=\"${x(q)}\" y=\"226\" text-anchor=\"middle\">${viz.number(q,1)}</text>`;\n for(const v of [0,a.ymax/2,a.ymax])svg+=`<text x=\"39\" y=\"${y(v)+4}\" text-anchor=\"end\">${viz.number(v,1)}</text>`;\n svg+=`<text x=\"48\" y=\"20\">${escape(viz.t(a.y))}</text><text x=\"326\" y=\"251\" text-anchor=\"end\">${escape(viz.t(a.x))}</text>`;\n (a.areas||[]).forEach((region,j)=>{svg+=`<polygon points=\"${region.points.map(([xx,yy])=>x(xx)+','+y(yy)).join(' ')}\" fill=\"${j===0?'#4c8c9d':'#bd7446'}\" fill-opacity=\".30\"/>`;});\n a.lines.forEach((l,j)=>{svg+=`<polyline fill=\"none\" stroke=\"${col[j]}\" stroke-width=\"${l.key==='wedge'?5:2.5}\" ${l.dash?'stroke-dasharray=\"5 4\"':''} points=\"${l.points.map(([xx,yy])=>x(xx)+','+y(yy)).join(' ')}\"/>`;});\n if(a.dot)svg+=`<circle cx=\"${x(a.dot[0])}\" cy=\"${y(a.dot[1])}\" r=\"5\" fill=\"var(--ink)\"/>`;\n el.innerHTML=svg+'</svg><div class=\"legend\">'+a.lines.map((l,j)=>`<span><i style=\"background:${col[j]}\"></i>${escape(viz.t(l.key))}</span>`).join('')+(a.areas||[]).map((a,j)=>`<span><i style=\"height:10px;background:${j===0?'#4c8c9d':'#bd7446'}\"></i>${escape(viz.t(a.key))}</span>`).join('')+'</div>';\n }else if(r.bars){el.innerHTML=r.bars.map(b=>`<div class=\"barrow\"><div class=\"barlabel\"><span>${escape(b.key?viz.t(b.key):viz.t('round')+' '+b.label)}</span><strong>${viz.number(b.v)}</strong></div><div class=\"bar\"><i style=\"width:${Math.abs(b.v)/Math.max(r.max,...r.bars.map(a=>Math.abs(a.v)))*100}%\"></i></div></div>`).join('')+(N===22?`<p class=\"scope\">${escape(viz.t('negative'))}</p>`:'');\n }else if(r.stack){el.innerHTML='<div class=\"stack\">'+r.stack.map((v,j)=>`<span style=\"width:${v}%;background:${col[j]}\">${v}</span>`).join('')+'</div><div class=\"legend\">'+r.stackKeys.map((k,j)=>`<span><i style=\"background:${col[j]}\"></i>${escape(viz.t(k))}</span>`).join('')+'</div>';}\nrenderTeachingView(r,el);\n}\nfor(const k of Object.keys(DEFAULTS))$('#'+k).addEventListener('input',render);\n$('#reset').addEventListener('click',()=>{for(const[k,v]of Object.entries(DEFAULTS))$('#'+k).value=v;render();});render();\n","audio":false,"strings":{"elasticity":"中点需求弹性绝对值","figure":"当前参数下的模型图;准确值见下方结果。","lostRevenue":"失去的销量少收","newRevenue":"新营业收入","oldRevenue":"原营业收入","price":"单位价格 P","quantity":"数量 Q","remaining":"涨价后留下的销量 / 件","reset":"恢复基准","result":"计算结果","retainedBase":"留下销量的原支付","retainedGain":"留下的销量多付","scope":"价格从 10 到 12,原销量 100。留下 90 件时多收 180、少收 100,收入净增 80;留下 70 件时净减 160。蓝色是保留部分,绿色是多收,橙色是失去。"}}
绿色面积是保留销量上的提价增收,橙色是消失销量带走的原收入。把留下的数量从 90 改到 70,观察两块面积谁更大。收入不变的分界是 12Q=1000,即留下约 83.33 份;连续数量只是分析近似,整份销售时对应相邻整数两侧。
弹性把不同规模的反应放到同一尺度
单说少卖 10 份,对一家日卖 20 份的店和一家日卖 1000 份的店,意义不同。需求价格弹性比较数量与价格的百分比变化。比较两个端点时,我们用中点法,让正着算与反着算使用同一基准:
数量变化率 = (Q1−Q0) / ((Q1+Q0)/2)
价格变化率 = (P1−P0) / ((P1+P0)/2)
需求价格弹性绝对值 = |数量变化率 / 价格变化率|
从 10 到 12,价格变化率为 2/11≈18.18%。留下 90 份时,数量变化率为 −10/95≈−10.53%,弹性绝对值约 0.58;留下 70 份时为 |(-30/85)/(2/11)|≈1.94。前者小于 1,数量百分比反应较弱,称缺乏弹性;后者大于 1,称富有弹性。
单位弹性是增收和减收刚好抵消的分界。它针对这段价格变化、这个市场和这段时间,不是商品永远不变的标签。换“份”为“千份”会改变曲线的数值斜率,却不改变百分比弹性;同一直线上,基点不同,百分比反应也会不同。
营业收入仍不等于利润。即使降价多卖、收入增加,多做的早餐也消耗原料和人工。下一篇先看交易双方的得失,企业篇再把成本加入定价;不要把这张收入图当成最优定价器。
把一个真实条件放回来:替代选择与时间
若旁边新开一家同类店,本店顾客更容易离开;研究“本店早餐”的反应与“全城早餐”的反应不是同一个问题。短期通勤者可能仍走原路线,几个月后可以改变通勤和用餐习惯,弹性因此也有时间范围。供给方短期设备固定,长期能扩充,同样要区分反应期限。
收入弹性问收入改变时买多少,交叉价格弹性问另一商品变价时买多少。它们改变了本商品需求关系的条件,不要与“本商品价格变化、其他条件不变”的弹性混算。现实两年的价格与销量之比,若同时包含收入、人口、质量变化,尚未识别价格自己的作用。
换个场景试一试
门票从 20 元降到 18 元,销量从 100 到 110。多卖十张是否意味着收入上升?先拆分支付变化,再计算中点弹性。
展开推理与易错点
原有 100 张各少收 2 元,减少 200;新增 10 张按新价各收 18,增加 180,净减 20。收入从 2000 到 1980。弹性绝对值为 (10/105)/(2/19)≈0.90,与缺乏弹性区间降价减少收入一致。降价的拆分与上面的提价拆分要选一致的基准,不能把新增票按旧价计而又对全部新销量扣两元。
某城租金上涨 10%、出租数量上涨 5%。有人算出弹性 +0.5 并说需求曲线向上。你需要补什么证据?
展开推理与可做的比较
先查人口、收入、房源、区域组成和质量是否变化,并确认数量是存量、成交量还是新增租约。新人口可让需求整体右移,使租金和数量同升。若要估计价格作用,需要能区分其他条件的对照或识别设计;仅用两期增长率相除没有完成这一步。回答“还要更多数据”不够,要说明哪项数据用来排除哪个解释。
资料与延伸阅读
以下资料用于核对概念与统计口径;本文数字、算例和图形为原创教学设定。
CORE · Firms and Markets for Goods and Services · OpenStax · Price Elasticity